Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/scripts/dev/data.json
- apps/web/app/ee/api/cron/import/bitly/route.ts
- apps/web/app/api/workspaces/idOrSlug/import/rebrandly/route.ts
- apps/web/lib/rewardful/importer.ts
- apps/web/app/ee/api/cron/import/rebrandly/route.ts
- apps/web/lib/rewardful/import-commissions.ts
- apps/web/app/ee/api/cron/import/rewardful/route.ts
- apps/web/app/ee/api/cron/import/tolt/route.ts
- apps/web/app/ee/api/cron/import/firstpromoter/route.ts
- apps/web/app/api/workspaces/idOrSlug/import/short/route.ts
- apps/web/app/ee/api/cron/import/short/route.ts
- apps/web/app/api/workspaces/idOrSlug/import/bitly/route.ts
- apps/web/lib/tapfiliate/importer.ts
- apps/web/lib/tolt/import-commissions.ts
- apps/web/lib/middleware/link.ts
- apps/web/app/api/callback/bitly/route.ts
- apps/web/app/ee/api/cron/import/lemonsqueezy/route.ts
- apps/web/lib/rewardful/import-campaigns.ts
- apps/web/ui/guides/integrations.ts
- apps/web/lib/rewardful/import-partners.ts
- apps/web/lib/firstpromoter/import-commissions.ts
- apps/web/lib/dub.ts
- apps/web/lib/firstpromoter/import-partners.ts
- apps/web/lib/partnerstack/importer.ts
- packages/utils/src/constants/dub-domains.ts
- apps/web/lib/middleware/utils/crawl-bitly.ts
- apps/web/lib/rewardful/import-customers.ts
- apps/web/lib/tolt/import-customers.ts
- apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts
- apps/web/scripts/customers/beehiiv/fix-case-a-simple.ts
Overview
Data Importers facilitate the migration of historical link, partner, customer, and commission data from external URL shorteners and affiliate management platforms into Dub. By providing automated ingestion pathways, the system bridges legacy external infrastructures with Dub's workspace environment and partner programs.
Sources: apps/web/app/api/workspaces/idOrSlug/import/bitly/route.ts:60-140, apps/web/app/ee/api/cron/import/rewardful/route.ts:13-37
Workspace link import endpoints
Provider Route Architecture
Dub provides authenticated workspace-scoped HTTP routes to connect, inspect, and initiate link migrations from external URL shorteners including Bitly, Rebrandly, and Short.io. Each provider integration exposes standard HTTP verbs (GET, PUT, POST) guarded by workspace authorization checks and specific permission requirements such as links.write and folders.links.write.
Sources: apps/web/app/api/workspaces/idOrSlug/import/bitly/route.ts:13-143, apps/web/app/api/workspaces/idOrSlug/import/rebrandly/route.ts:13-165, apps/web/app/api/workspaces/idOrSlug/import/short/route.ts:13-147
The migration endpoints perform token validation against secure Redis storage, query external provider APIs to fetch domains, groups, and link counts, and provision missing custom domains into both Prisma and Vercel prior to dispatching background ingestion jobs via QStash.
| Provider | Access Token Redis Key | Base API Endpoint | Required Permission |
|---|---|---|---|
| Bitly | import:bitly:{workspaceId} | https://api-ssl.bitly.com/v4 | links.write |
| Rebrandly | import:rebrandly:{workspaceId} | https://api.rebrandly.com/v1 | links.write |
| Short.io | import:short:{workspaceId} | https://api.short.io | links.write |
Sources: apps/web/app/api/workspaces/idOrSlug/import/bitly/route.ts:15-22, apps/web/app/api/workspaces/idOrSlug/import/rebrandly/route.ts:15-23, apps/web/app/api/workspaces/idOrSlug/import/short/route.ts:15-23
Note
During domain selection in POST handlers, if a selected import domain does not yet exist in the workspace, the route automatically registers it with Prisma (primary: false), provisions it on Vercel via addDomainToVercel, and generates a root placeholder link (_root) before publishing background cron tasks.
Sources: apps/web/app/api/workspaces/idOrSlug/import/bitly/route.ts:72-101, apps/web/app/api/workspaces/idOrSlug/import/rebrandly/route.ts:116-142, apps/web/app/api/workspaces/idOrSlug/import/short/route.ts:99-125
On-demand Bitly edge crawling
Edge Crawl Execution Flow
When an incoming request cannot be resolved through standard database lookups during edge redirect dispatch, the crawlBitly utility intercepts requests to evaluate whether the requested key matches Bitly shortlink structures. Keys containing unsupported characters specified by Bitly's character regex are immediately rejected from being crawled.
Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:20-27
const invalidBitlyKeyRegex = /[`~,.<>;':"/\\[\]^{}()=+!*@&$£?%#|]/;Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:25-25
For valid keys, the function queries the Bitly API endpoint using the configured BUFFER_BITLY_API_KEY. If the remote bitlink exists and returns a validated long URL via getUrlFromStringIfValid, the system provisions the link directly into the database as an on-demand creation assigned to dedicated buffer workspace identifiers.
| Constant Name | Value / Identifier | Description |
|---|---|---|
BUFFER_WORKSPACE_ID | cm05wnnpo000711ztj05wwdbu | Workspace assignment for on-demand crawled links |
BUFFER_USER_ID | cm05wnd49000411ztg2xbup0i | User ID associated with crawled link creation |
BUFFER_FOLDER_ID | fold_1JNQBVZV8P0NA0YGB11W2HHSQ | Folder categorization ID for buffer imports |
Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:15-17, apps/web/lib/middleware/utils/crawl-bitly.ts:35-50
Warning
If the Bitly API encounters a rate limit or returns a non-ok response during fetchBitlyLink, the handler logs the rate limit hit and returns a 404 status for the target domain and key combination rather than failing the edge request outright.
Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:100-105
Background Persistence and Fallback Routing
Once a bitlink record is retrieved from the Bitly API and sanitized, ev.waitUntil schedules background persistence tasks using Prisma to create the link entry, record analytics via Tinybird through recordLink, and publish workspace usage events to Upstash Redis streams via publishWorkspaceLinksUsageEvent.
Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:36-61
.then((data) =>
Promise.allSettled([
recordLink(data),
publishWorkspaceLinksUsageEvent({
workspaceId: BUFFER_WORKSPACE_ID,
linksCount: 1,
timestamp: new Date().toISOString(),
}),
]),
),Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:51-60
If the remote lookup fails or yields no valid link, the middleware issues a fallback redirect to https://buffer.com with a 302 status code and attaches static Vercel CDN cache headers with an s-maxage of 86400 seconds alongside cache tags generated via linkCache._createStaticPagesCacheKeys.
Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:70-81
return NextResponse.redirect("https://buffer.com", {
headers: {
...DUB_HEADERS,
"Vercel-CDN-Cache-Control": "public, s-maxage=86400",
"Vercel-Cache-Tag": linkCache._createStaticPagesCacheKeys({
domain,
key,
}),
},
status: 302,
});Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:71-81
Asynchronous link import workers
QStash Signature Verification and Cron Routing
Background synchronization for external provider migrations relies on POST endpoints configured with force-dynamic rendering. Every incoming request body is intercepted, read as text, and validated against QStash cryptographic signatures via verifyQstashSignature before JSON parsing occurs.
Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:12-19, apps/web/app/ee/api/cron/import/rebrandly/route.ts:9-16, apps/web/app/ee/api/cron/import/short/route.ts:9-16
export async function POST(req: Request) {
try {
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });
const body = JSON.parse(rawBody);Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:14-19
Depending on the provider target, requests parse specific parameters from the payload to drive batch processing and handle authentication credentials stored inside Redis keys.
| Provider Route | Payload Parameters Extracted | Redis Key Lookup Pattern |
|---|---|---|
| Bitly | workspaceId, bitlyGroup, importTags, rateLimited | import:bitly:{workspaceId} |
| Rebrandly | workspaceId, importTags, createdAfter | import:rebrandly:{workspaceId} |
| Short.io | workspaceId, userId, domainId, domain, folderId, importTags, pageToken, count | import:short:{workspaceId} |
Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:19-23, apps/web/app/ee/api/cron/import/rebrandly/route.ts:16-22, apps/web/app/ee/api/cron/import/short/route.ts:16-26
Caution
If a Rebrandly API key cannot be retrieved from Redis during background processing, the handler immediately throws a DubApiError with a bad_request code rather than attempting unauthenticated remote fetches.
Sources: apps/web/app/ee/api/cron/import/rebrandly/route.ts:20-29
Tag Synchronization and Rate Limiting Flow
When import routines execute with importTags enabled, workers check Redis for previously cached flags (import:bitly:${workspaceId}:tags or import:rebrandly:${workspaceId}:tags). If absent, tag definitions are fetched from the upstream provider API, inserted into Prisma via createId with generated badge colors, and mapped to database identifiers.
Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:35-82, apps/web/app/ee/api/cron/import/rebrandly/route.ts:31-62
await prisma.tag.createMany({
data: tags.map((tag) => ({
id: createId({ prefix: "tag_" }),
name: tag,
color: randomBadgeColor(),
projectId: workspaceId,
})),
skipDuplicates: true,
});
await redis.set(`import:bitly:${workspaceId}:tags`, "true");
}Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:54-64
Bitly background workers also evaluate provider-specific rate limits when the rateLimited flag evaluates to true, returning a JSON response indicating rate_limited status when thresholds are exceeded.
Sources: apps/web/app/ee/api/cron/import/bitly/route.ts:25-33
Note
OAuth callback handoff sequences for Bitly store access tokens directly in Redis under import:bitly:${workspaceId} before redirecting users back to their workspace dashboard with query parameters configured for initialization.
Sources: apps/web/app/api/callback/bitly/route.ts:46-53
Affiliate partner ingestion workflow
Execution Walkthrough and Sequence
Affiliate partner migration pipelines ingest programs, campaigns, and active partners from platforms like Rewardful and FirstPromoter via QStash-verified background cron routes. The execution router evaluates the incoming POST action payload to dispatch processing across specific handlers for campaigns, partners, affiliate coupons, customers, and commissions.
Sources: apps/web/app/ee/api/cron/import/rewardful/route.ts:13-36
-
POST— The QStash cron endpoint verifies message signatures and parses payload actions, routing execution toimportPartnersfor active affiliate ingestion viaapps/web/app/(ee)/api/cron/import/rewardful/route.ts. Sources: apps/web/app/ee/api/cron/import/rewardful/route.ts:13-26 -
importPartners— Fetches workspace credentials from Redis, instantiates provider clients, pages through remote affiliate records, filters active partners with existing leads or matching campaign IDs, and delegates creation tocreatePartnerAndLinksviaapps/web/lib/rewardful/import-partners.ts. Sources: apps/web/lib/rewardful/import-partners.ts:14-110, apps/web/lib/firstpromoter/import-partners.ts:19-102 -
createPartnerAndLinks— Upserts partner database records by email, establishes approved program enrollments with group attributes, invokes application approval handlers, and bulk-creates referral links viaapps/web/lib/rewardful/import-partners.ts. Sources: apps/web/lib/rewardful/import-partners.ts:165-256, apps/web/lib/firstpromoter/import-partners.ts:116-254
Sources: apps/web/app/ee/api/cron/import/rewardful/route.ts:13-26, apps/web/lib/rewardful/import-partners.ts:14-256
Platform Integration Parameters and Limits
| Importer Platform | Maximum Batch Limit | Credential Cache Duration | Default Target Entity |
|---|---|---|---|
| Rewardful | 10 batches (REWARDFUL_MAX_BATCHES) | 24 hours (86400 seconds) | partner |
| FirstPromoter | 5 batches (MAX_BATCHES) | 24 hours | partner |
Sources: apps/web/lib/rewardful/importer.ts:8-13, apps/web/lib/rewardful/import-partners.ts:45-52, apps/web/lib/firstpromoter/importer.ts:8-10, apps/web/lib/firstpromoter/import-partners.ts:52-56
Warning
When FirstPromoter link creation fails during partner imports, the database enrollment remains committed; error catch blocks ensure the enrollment identifier is still pushed to page-level partner search synchronization queues.
Sources: apps/web/lib/firstpromoter/import-partners.ts:240-252
Customer referral ingestion pipeline
Ingestion Flow and Synthetic Click Generation
Customer migrations from Rewardful and Tolt parse paginated referral payloads, verify existing database records by Stripe customer identifier or external ID, and manufacture synthetic click events for historical attribution. Because historical imports lack live HTTP requests, the pipeline constructs a dummy Request object with predefined headers (user-agent, x-forwarded-for, and Vercel geo headers) to simulate visitor traffic against the target link URL.
Sources: apps/web/lib/rewardful/import-customers.ts:31-75, apps/web/lib/rewardful/import-customers.ts:210-236, apps/web/lib/tolt/import-customers.ts:33-121, apps/web/lib/tolt/import-customers.ts:205-231
Sources: apps/web/lib/rewardful/import-customers.ts:36-90, apps/web/lib/rewardful/import-customers.ts:210-296, apps/web/lib/tolt/import-customers.ts:34-136, apps/web/lib/tolt/import-customers.ts:205-292
Platform-Specific Ingestion Rules
| Importer Platform | Batch Limit Constant | Link & Token Resolution | Customer Deduplication Keys |
|---|---|---|---|
| Rewardful | REWARDFUL_REFERRALS_MAX_BATCHES | referral.link?.token or referral.coupon?.token with -coupon suffix | stripeCustomerId, workspace externalId |
| Tolt | MAX_BATCHES | Partner email mapped to program enrollment links (links[0]) | Workspace externalId (`customer_id |
Sources: apps/web/lib/rewardful/import-customers.ts:35, apps/web/lib/rewardful/import-customers.ts:153-169, apps/web/lib/rewardful/import-customers.ts:194-208, apps/web/lib/tolt/import-customers.ts:33, apps/web/lib/tolt/import-customers.ts:83-94, apps/web/lib/tolt/import-customers.ts:111-120, apps/web/lib/tolt/import-customers.ts:195-201
Caution
Rewardful customer records missing a valid Stripe customer ID starting with cus_ trigger a STRIPE_CUSTOMER_NOT_FOUND import error log and halt processing for that specific referral.
Sources: apps/web/lib/rewardful/import-customers.ts:181-192
Important
When processing Rewardful referrals, chunks of 10 items are handled concurrently via Promise.allSettled, while Tolt batches include a 2-second sleep(2000) delay between pages to respect upstream rate limits.
Sources: apps/web/lib/rewardful/import-customers.ts:77-90, apps/web/lib/tolt/import-customers.ts:122-138
Commission sync and attribution updates
Historical commissions are backfilled via asynchronous workers triggered through QStash endpoints in cron routes such as apps/web/app/(ee)/api/cron/import/firstpromoter/route.ts, apps/web/app/(ee)/api/cron/import/rewardful/route.ts, and apps/web/app/(ee)/api/cron/import/tolt/route.ts. During commission processing, sale amounts expressed in foreign currencies are converted to USD utilizing real-time exchange rates fetched from Upstash Redis hashes (fxRates:usd).
let saleAmount = Number(commission.original_sale_amount ?? 0);
const saleCurrency = commission.original_sale_currency ?? "usd";
if (saleAmount > 0 && saleCurrency.toUpperCase() !== "USD" && fxRates) {
const { amount: convertedAmount } = convertCurrencyWithFxRates({
currency: saleCurrency,
amount: saleAmount,
fxRates,
});
saleAmount = convertedAmount;
}Sources: apps/web/lib/firstpromoter/import-commissions.ts:231-242, apps/web/lib/rewardful/import-commissions.ts:48, apps/web/lib/tolt/import-commissions.ts:52
Caution
Commissions with units other than cash or those flagged as self-referrals are rejected and logged via Tinybird import error tracking with specific error codes like SELF_REFERRAL or NOT_SUPPORTED_UNIT.
Sources: apps/web/lib/firstpromoter/import-commissions.ts:181-199
Status mappings vary across upstream affiliate providers to unify commission states within Dub databases:
| Provider | Upstream Status Value | Mapped Dub Status |
|---|---|---|
| Rewardful | pending, due | pending |
| Rewardful | paid | paid |
| Rewardful | voided | canceled |
| Tolt | pending, approved | pending |
| Tolt | paid | paid |
| Tolt | rejected | canceled |
| Tolt | refunded | refunded |
| FirstPromoter | pending, approved | pending |
| FirstPromoter | denied | canceled |
Sources: apps/web/lib/rewardful/import-commissions.ts:28-33, apps/web/lib/tolt/import-commissions.ts:28-34, apps/web/lib/firstpromoter/import-commissions.ts:28-33
Tip
Downstream analytics records are updated concurrently with Prisma database mutations by calling recordSaleWithTimestamp, passing parsed click event schemas, customer identifiers, and payment processor details set to stripe.
Sources: apps/web/lib/firstpromoter/import-commissions.ts:320-365
Related
On This Page
OverviewWorkspace link import endpointsProvider Route ArchitectureOn-demand Bitly edge crawlingEdge Crawl Execution FlowBackground Persistence and Fallback RoutingAsynchronous link import workersQStash Signature Verification and Cron RoutingTag Synchronization and Rate Limiting FlowAffiliate partner ingestion workflowExecution Walkthrough and SequencePlatform Integration Parameters and LimitsCustomer referral ingestion pipelineIngestion Flow and Synthetic Click GenerationPlatform-Specific Ingestion RulesCommission sync and attribution updatesRelated