Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
The Shopify integration for Dub enables real-time conversion analytics and affiliate attribution by connecting Shopify merchants to workspaces. It manages the complete lifecycle of customer interactions, including OAuth installation callbacks, client-side web pixel tracking, server-side webhook ingestion, asynchronous order processing, discount code attribution, and commission publishing.
Sources: apps/web/lib/integrations/shopify/schema.ts:1-51, apps/web/app/ee/api/shopify/integration/callback/route.ts:25-90, apps/web/lib/jobs/handlers/process-shopify-order-job.ts:17-68
The Shopify integration handshake is managed through the PATCH endpoint located at apps/web/app/ee/api/shopify/integration/callback/route.ts. This route handles workspace authentication, validates incoming payload schemas using Zod discriminated unions, updates project configuration in Prisma, and securely persists encrypted OAuth access tokens along with granted scopes.
Incoming requests are parsed via requestSchema, which defines a discriminated union on the action property supporting store connections and disconnections.
Important
Access to the callback route is strictly guarded by workspace middleware (withWorkspace). Callers must possess either the owner or member role and belong to workspaces subscribed to business, advanced, or enterprise plans.
When an authenticated PATCH request hits the callback handler, execution proceeds through validation, database updates, credential encryption, and integration record management:
parseRequestBody(req) — Extracts and parses the incoming HTTP request body.requestSchema.parse(...) — Validates the payload against the discriminated union schema for connect or disconnect actions.prisma.project.update(...) — Updates the workspace project record in the database, setting or clearing the shopifyStoreId.installIntegration(...) (conditional on body.action === "connect"):
encrypt(body.accessToken) — Encrypts the raw Shopify OAuth access token prior to persistence.InstalledIntegration record binding the user, workspace, SHOPIFY_INTEGRATION_ID, and credentials.prisma.installedIntegration.delete(...) (conditional on body.action === "disconnect"):
userId_integrationId_projectId), silently catching any errors if absent.Warning
If a Prisma database operation throws error code P2002 (indicating a unique constraint violation), the handler catches it and throws a DubApiError with the conflict code, signaling that the specified Shopify store is already tied to another project.
Client-side checkout and click tracking are handled by the Shopify Web Pixel route at apps/web/app/ee/api/shopify/pixel/route.ts. This endpoint receives tracking payloads from the storefront web pixel, validates request inputs, enforces rate limits, verifies underlying click events, and caches checkout-to-click associations in Redis before evaluating whether to dispatch asynchronous order processing jobs.
Incoming POST requests are parsed using a Zod schema that expects three optional string properties: clickId, checkoutToken, and shopDomain.
Warning
Both checkoutToken and clickId are strictly required to proceed. If either field is missing or nullish, the route logs an error and immediately returns an HTTP "OK" response without performing further caching or job dispatch.
When a valid pixel tracking request is received, execution proceeds through security checks, database lookups, and asynchronous cache updates:
parseRequestBody(req) & inputSchema.parse(...) — Extracts and validates the incoming JSON payload against expected fields.ratelimit().limit(...) — Applies an Upstash rate limit keyed by shopify-track-pixel:${ip}, where the IP is derived via ipAddress(req) on Vercel or LOCALHOST_IP in local development.getClickEvent({ clickId }) — Queries Tinybird to verify that the specified clickId exists in the click event store.waitUntil(...) — Schedules background execution on Vercel to decouple downstream cache persistence from the HTTP response:
shopifyCheckoutCache.set({ checkoutToken, fields: { clickId } }) — Writes the checkout-to-click association into Redis using a pipeline (hset, expire, hgetall) with a TTL of 1 hour (SHOPIFY_CHECKOUT_CACHE_TTL_SECONDS).tryDispatchShopifyOrderJob({ checkoutToken, checkout }) — Inspects cached checkout state and attempts to trigger the order processing job if all required fields are present.Sources: apps/web/app/ee/api/shopify/pixel/route.ts:26-75, apps/web/lib/integrations/shopify/checkout-cache.ts:6-43, apps/web/lib/integrations/shopify/checkout-cache.ts:61-128
Tip
The tryDispatchShopifyOrderJob helper uses an atomic Redis hsetnx operation on the dispatched field to claim the checkout. This prevents duplicate job dispatches when both the client-side web pixel event and the server-side orders-paid webhook arrive concurrently.
Server-side webhook ingestion, HMAC validation, and job dispatch orchestration are handled by the Shopify webhook route at apps/web/app/ee/api/shopify/integration/webhook/route.ts and its topic handlers. This route processes incoming Shopify webhook events, verifies HMAC cryptographic signatures, validates workspace configurations, and routes payloads to specific handlers such as ordersPaid in apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts.
Sources: apps/web/app/ee/api/shopify/integration/webhook/route.ts:25-155, apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125
The POST endpoint begins by extracting raw request text, headers, and Shopify-specific metadata including x-shopify-topic, x-shopify-hmac-sha256, and x-shopify-shop-domain.
Warning
Unless running in local development (isLocalDev), the webhook route computes an HMAC SHA256 digest of the raw request body using SHOPIFY_WEBHOOK_SECRET and compares it to the incoming signature using timingSafeCompare. If verification fails, the route immediately returns an HTTP 401 response.
The system tracks specific mandatory compliance topics and order events, which are validated against a predefined relevantTopics set before workspace lookup and dispatch occur.
Sources: apps/web/app/ee/api/shopify/integration/webhook/route.ts:15-23, apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-126
When an orders/paid webhook event is successfully authenticated and matched to a workspace, execution flows through the ordersPaid handler and checkout cache manager:
POST — The main webhook route receives the HTTP request, validates headers, verifies the HMAC signature, parses the event JSON, and dispatches control to ordersPaid based on the orders/paid topic.ordersPaid — Parses the order using shopifyOrderSchema, checks for existing customer records or matching partner discount codes, and falls back to checking note_attributes for a dubClickId before writing to cache or queuing processing.tryDispatchShopifyOrderJob — Evaluates whether the cached checkout contains all required fields (order, workspaceId, clickId) and is not already dispatched, then attempts an atomic Redis hsetnx claim on the dispatched field.delete — Upon successful job dispatch, shopifyCheckoutCache.delete(checkoutToken) removes the temporary checkout entry from Redis.createKey — The cache helper generates the underlying Redis key using shopifyCheckoutCache.createKey(checkoutToken), formatted with the shopify:checkout: prefix.Sources: apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-102, apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125, apps/web/lib/integrations/shopify/checkout-cache.ts:45-51, apps/web/lib/integrations/shopify/checkout-cache.ts:61-128
Sources: apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-102, apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125, apps/web/lib/integrations/shopify/checkout-cache.ts:45-51, apps/web/lib/integrations/shopify/checkout-cache.ts:61-128
Note
Unlike other webhook topics whose request logs are captured immediately within the main route handler using waitUntil and captureWebhookLog, orders/paid log capture is deferred and handled directly by processShopifyOrderJob after the order completes processing.
Background order reconciliation executes asynchronously through the queue worker handler to resolve orders against existing customer records, partner program discount codes, or web pixel click attribution identifiers.
Sources: apps/web/lib/jobs/handlers/process-shopify-order-job.ts:17-20, apps/web/lib/integrations/shopify/process-order.ts:10-18
When processShopifyOrderJob picks up a queued order payload, execution flows through the worker handler and reconciliation router:
processShopifyOrderJob — The job handler fetches workspace configuration via prisma.project.findUniqueOrThrow, initializes request logging parameters, and invokes processShopifyOrder.processShopifyOrder — Inspects incoming order properties, checking first for an existing customer record, then evaluating workspace discount codes, and finally falling back to click ID tracking.prisma.customer.findUnique — Queries the database for an existing customer using the compound key projectId_externalId.getLeadEvent — Retrieves the customer lead event from Tinybird when an existing customer match is confirmed.createShopifySale — Records the sale event linked to resolved customer and lead data.Sources: apps/web/lib/jobs/handlers/process-shopify-order-job.ts:20-48, apps/web/lib/integrations/shopify/process-order.ts:10-58
Sources: apps/web/lib/jobs/handlers/process-shopify-order-job.ts:20-48, apps/web/lib/integrations/shopify/process-order.ts:10-58
The reconciliation engine routes orders through three distinct attribution branches depending on customer state, discount code usage, and pixel tracking data.
Warning
When an existing customer record is located but the corresponding Tinybird lead event cannot be fetched (!leadData), processShopifyOrder explicitly throws an error rather than skipping the order. This ensures the background job retries instead of duplicating customer records.
When an incoming Shopify order lacks an explicit click tracking identifier (clickId) or existing customer record, attribution falls back to affiliate discount codes applied at checkout. The attributeViaDiscountCode function handles matching discount codes to affiliate links, generating synthetic traffic records, and propagating conversion metrics across analytics and partner systems.
Because orders attributed via discount codes lack a pre-existing browser click event, the platform manufactures synthetic records to maintain relational integrity across analytics backends. The billing address country code is resolved against COUNTRIES_TO_CONTINENTS to build geographic context for a fake click.
const clickEvent = await recordFakeClick({
link,
customer: {
continent: billingAddressCountry
? COUNTRIES_TO_CONTINENTS[billingAddressCountry] ?? "Unknown"
: "Unknown",
country: billingAddressCountry ?? "Unknown",
region: billingAddress?.province ?? "Unknown",
},
});Important
The database customer record is created before the lead event is pushed to Tinybird. This ordering guarantees that a database constraint violation on projectId_externalId (P2002) will never leave behind an orphaned Tinybird click record.
The attribution flow executes through a series of distinct asynchronous operations, orchestrating local database persistence, analytics recording, and downstream webhooks:
attributeViaDiscountCode — Entry point receiving the order, workspace, and matched link.recordFakeClick — Generates a synthetic click entry in Tinybird using extracted billing geography.prisma.customer.create — Persists the newly attributed customer tied to link.id, link.programId, and link.partnerId.recordLead — Transmits the manufactured lead event (Checkout with discount code) to Tinybird with a generated nanoid(16) event ID.prisma.link.update — Increments lead counters on the associated link and updates lastLeadAt.queuePartnerCommissionCreation — Enqueues a partner commission when link.programId and link.partnerId are present.Promise.allSettled — Dispatches parallel notification tasks including workspace webhooks (sendWorkspaceWebhook), Google Ads conversion uploads (queueGoogleAdsConversionUpload), and partner postbacks (sendPartnerPostback).Discount provider operations manage the lifecycle of Shopify discount codes via GraphQL mutations. When creating discount codes using discountCodeBasicCreate, duplicate code collisions trigger an automated retry mechanism up to MAX_ATTEMPTS (3 attempts), appending a 2-character nanoid to the conflicting code string.
Warning
If a discount code creation attempt fails with an unrecognized error code or exceeds MAX_ATTEMPTS during collision resolution, a DiscountProviderError with type CREATE_FAILED is thrown, aborting the transaction.
Once an order has been successfully attributed to a customer and link, the integration transitions to recording the financial transaction and publishing associated partner commissions. This step processes the monetary payload from the Shopify order, enforces idempotency checks via Redis cache keys, writes analytics records to Tinybird, updates link and customer aggregates, and asynchronously dispatches downstream workspace webhooks and partner postbacks.
The sale record and commission publishing flow executes through a structured sequence of checks, data mutations, and asynchronous dispatches:
createShopifySale — Entry point receiving the normalized order, customerId, workspaceId, and leadData.redis.set — Checks and sets a 7-day TTL idempotency lock using key format dub_sale_events:linkId:${linkId}:invoiceId:${invoiceId} with nx: true to prevent duplicate processing of the same invoice.prisma.customer.findUniqueOrThrow — Retrieves the existing customer record and evaluates conversion status via isFirstConversion.Promise.all — Executes atomic parallel persistence:
recordSale(saleData) — Writes the sale event to Tinybird.prisma.link.update — Increments link sales counts, sale amounts, and conditionally increments conversions and lastConversionAt.prisma.project.update — Increments workspace usage metrics.prisma.customer.update — Updates customer sales aggregates and sets firstSaleAt if unpopulated.shopifyCheckoutCache.delete(checkoutToken) — Clears the temporary checkout cache entry.queuePartnerCommissionCreation — Enqueues partner commission creation when link.programId and link.partnerId are present, specifying CommissionSource.shopify.waitUntil — Triggers background processing via Promise.allSettled for workflow execution (executeWorkflows), link stats synchronization (syncPartnerLinksStats), workspace webhooks (sendWorkspaceWebhook), and partner postbacks (sendPartnerPostback).Important
The Redis idempotency check uses nx: true with a 7-day expiration (ex: 60 * 60 * 24 * 7) keyed on linkId and invoiceId. If the key already exists, a ShopifyError is thrown immediately to abort duplicate order processing.
The sale event object constructed before persistence contains specific properties mapped from the Shopify order and lead context: