Skip to main content
doc0blade47/dubdocumentation home
Sign in

Embeddable Referrals

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Embeddable Referrals provide a secure mechanism for integrating white-labeled partner and affiliate program dashboards directly into external web applications via iframe wrappers and React components. By minting cryptographically bound tokens backed by Redis storage, platforms can authorize external tenants to track link performance, view earnings, manage referral links, and submit bounties without exposing internal credentials.

Note

Embedding requires a valid public embed token passed through the authorization header or query parameters to authenticate embedded partner contexts against backend API routes.

Sources: apps/web/lib/embed/referrals/token-class.ts:13-33, packages/embeds/react/src/embed.tsx:8-12, apps/web/lib/embed/referrals/auth.ts:33-128

Embed Token Minting

Token Generation and Storage

The token minting process is coordinated through the ReferralsEmbedToken class, which uses Upstash Redis storage to persist session properties mapped to a unique public identifier. When create() is invoked with a programId and partnerId, a secure identifier is generated using createId() prefixed with EMBED_PUBLIC_TOKEN_PREFIX. This token is stored in Redis containing the JSON-serialized payload along with an expiration configured by EMBED_PUBLIC_TOKEN_EXPIRY, enforcing non-collision requirements via the nx option.

class ReferralsEmbedToken {
  async create(props: ReferralsEmbedTokenProps) {
    const publicToken = createId({
      prefix: EMBED_PUBLIC_TOKEN_PREFIX,
    });
 
    await redis.set(publicToken, JSON.stringify(props), {
      ex: EMBED_PUBLIC_TOKEN_EXPIRY,
      nx: true,
    });
 
    return {
      publicToken,
      expires: new Date(Date.now() + EMBED_PUBLIC_TOKEN_EXPIRY * 1000),
    };
  }
 
  async get(token: string) {
    return await redis.get<ReferralsEmbedTokenProps>(token);
  }
}

Sources: apps/web/lib/embed/referrals/token-class.ts:13-33

Warning

Stored embed tokens rely entirely on EMBED_PUBLIC_TOKEN_EXPIRY for time-to-live management in Redis. If the Redis key expires, subsequent token resolution calls will fail to retrieve the underlying programId and partnerId mapping.

Server Endpoints and Upsert Workflows

Two primary API routes handle token generation depending on the execution context. The workspace-level endpoint at /tokens/embed/referrals validates requests using withWorkspace requiring links.write permissions and an advanced or enterprise plan. It attempts to locate an existing ProgramEnrollment by partnerId or tenantId. If no enrollment exists but partner properties are provided, it resolves or creates the partner record and enrolls them into the default program before minting the token.

// POST /api/tokens/embed/referrals - create a new embed token for the given partner/tenant
export const POST = withWorkspace(
  async ({ workspace, req, session }) => {
    const programId = getDefaultProgramIdOrThrow(workspace);
 
    const {
      partnerId,
      tenantId,
      partner: partnerProps,
    } = createReferralsEmbedTokenSchema.parse(await parseRequestBody(req));
 
    const finalTenantId =
      tenantId || partnerProps?.tenantId || partnerProps?.linkProps?.tenantId;
 
    let programEnrollment: Pick<ProgramEnrollment, "partnerId"> | null = null;
 
    // find the program enrollment for the given partnerId or tenantId
    if (partnerId || finalTenantId) {
      programEnrollment = await prisma.programEnrollment.findUnique({
        where: partnerId
          ? { partnerId_programId: { partnerId, programId } }
          : { tenantId_programId: { tenantId: finalTenantId!, programId } },
        select: {
          partnerId: true,
        },
      });
    }
 
    // if there is no programEnrollment associated with the partnerId or tenantId, and partnerProps are provided
    // check if the partner exists based on the email – if not, create them, if yes, enroll them
    if (!programEnrollment && partnerProps) {
      const program = await prisma.program.findUniqueOrThrow({
        where: {
          id: programId,
        },
        select: {
          id: true,
          workspaceId: true,
          defaultFolderId: true,
          domain: true,
          url: true,
          defaultGroupId: true,
        },
      });
 
      const partner = await prisma.partner.findUnique({
        where: {
          email: partnerProps.email,
        },
        include: {
          programs: {
            where: {
              programId,
            },
          },
        },
      });
 
      // partner does not exist, we need to create them OR
      // partner exists but is not enrolled in the program, we need to enroll them
      if (!partner || partner.programs.length === 0) {
        const { linkProps: link, ...partner } = partnerProps;
 
        const enrolledPartner = await createAndEnrollPartner({
          workspace,
          program,
          partner: {
            ...partner,
            ...(finalTenantId && { tenantId: finalTenantId }),
          },
          link: {
            ...link,
            ...(finalTenantId && { tenantId: finalTenantId }),
          },
          userId: session.user.id,
        });
 
        programEnrollment = {
          partnerId: enrolledPartner.id,
        };

Sources: apps/web/app/api/tokens/embed/referrals/route.ts:15-122

The user-level route at /api/user/referrals-token evaluates session state, checking paid workspace allocations, reputation criteria such as earned commissions and ban status via partnerHasEarnedCommissions and partnerIsNotBanned, and account age thresholds before issuing a public token for the user tenant.

export const GET = withSession(async ({ session }) => {
  const user = await prisma.user.findUniqueOrThrow({
    where: {
      id: session.user.id,
    },
    include: {
      partners: {
        include: {
          partner: {
            include: {
              programs: true,
            },
          },
        },
      },
      projects: {
        where: {
          project: {
            plan: {
              not: "free",
            },
          },
        },
      },
    },
  });
  const paidWorkspaces = user.projects.map((project) => project);
 
  // for free users, need to do some extra checks
  if (paidWorkspaces.length === 0) {
    // for partners with free workspaces, they are only eligible if they have a good reputation
    const programEnrollments = user.partners[0]
      ? user.partners[0].partner.programs
      : [];
    if (
      programEnrollments.length > 0 &&
      !(
        partnerHasEarnedCommissions(
          programEnrollments as ProgramEnrollmentsForDiscoverability,
        ) && partnerIsNotBanned(programEnrollments)
      )
    ) {
      return NextResponse.json({ publicToken: null });
    }
 
    // for regular free users, don't allow them to join the referral program if they've just joined (within the last 30 days)
    if (user.createdAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)) {
      return NextResponse.json({ publicToken: null });
    }
  }
 
  const { publicToken } = await dub.embedTokens.referrals({
    tenantId: session.user.id,
    partner: {
      name: session.user.name || session.user.email,
      email: session.user.email,
      image: session.user.image || null,
      tenantId: session.user.id,
      groupId: "grp_1K2QJWRQ917XX2YR5VHQ1RRC5", // User Referrals group
    },
  });
 
  return NextResponse.json({ publicToken });
});

Sources: apps/web/app/api/user/referrals-token/route.ts:11-74

OpenAPI Specification and Operations

The public API specification for embed token creation is defined using zod-openapi. The route registers under /tokens/embed/referrals with the createReferralsEmbedToken operation object, specifying request body validation schemas and response structures.

Operation PropertyValue / SchemaDescription
operationIdcreateReferralsEmbedTokenUnique identifier for SDK generation
x-speakeasy-name-overridereferralsSpeakeasy client naming override
summary"Create a referrals embed token"Short description of endpoint purpose
requestBodycreateReferralsEmbedTokenSchemaJSON schema validating partner and tenant payload
responses201, ...openApiErrorResponsesReturns ReferralsEmbedTokenSchema on success
tags["Embed Tokens"]OpenAPI categorization tag
security[{ token: [] }]API key security requirement
export const createReferralsEmbedToken: ZodOpenApiOperationObject = {
  operationId: "createReferralsEmbedToken",
  "x-speakeasy-name-override": "referrals",
  summary: "Create a referrals embed token",
  description:
    "Create a referrals embed token for the given partner/tenant. The endpoint first attempts to locate an existing enrollment using the provided tenantId. If no enrollment is found, it resolves the partner by email and creates a new enrollment as needed. This results in an upsert-style flow that guarantees a valid enrollment and returns a usable embed token.",
  requestBody: {
    content: {
      "application/json": {
        schema: createReferralsEmbedTokenSchema,
      },
    },
  },
  responses: {
    "201": {
      description: "The created public embed token.",
      content: {
        "application/json": {
          schema: ReferralsEmbedTokenSchema,
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["Embed Tokens"],
  security: [{ token: [] }],
};

Sources: apps/web/lib/openapi/embed-tokens/create-referrals-embed-token.ts:8-34, apps/web/lib/openapi/embed-tokens/index.ts:1-7

Token Verification Middleware

Authentication Wrappers and Route Handling

export const withReferralsEmbedToken = (
  handler: WithReferralsEmbedTokenHandler,
) => {
  return withAxiom(
    async (
      req,
      { params: initialParams }: { params: Promise<Record<string, string>> },
    ) => {
      const params = (await initialParams) || {};
      const requestHeaders = await headers();
      let responseHeaders = new Headers();
 
      try {
        const rateLimit = 60;
        const searchParams = getSearchParams(req.url);
        const embedToken = requestHeaders.get("Authorization")?.split(" ")[1];
 
        if (!embedToken) {
          throw new DubApiError({
            code: "unauthorized",
            message: "Embed public token not found in the request.",
          });
        }
 
        const { programId, partnerId } =
          (await referralsEmbedToken.get(embedToken)) ?? {};
 
        if (!programId || !partnerId) {
          throw new DubApiError({
            code: "unauthorized",
            message: "Invalid embed public token.",
          });
        }

Sources: apps/web/lib/embed/referrals/auth.ts:33-65

The withReferralsEmbedToken function acts as a higher-order wrapper securing referral embed API routes. It intercepts incoming requests to extract the public token from the Authorization request header using a Bearer scheme split. When the token is missing, it throws a DubApiError with the unauthorized code and a specific message. If the token is successfully extracted, it queries the underlying storage via referralsEmbedToken.get(embedToken) to retrieve the associated programId and partnerId.

Warning

If a request provides an invalid or expired embed token where programId or partnerId cannot be resolved from storage, the wrapper immediately halts execution and returns an unauthorized response.

const { success, limit, reset, remaining } = await ratelimit(
  rateLimit,
  "1 m",
).limit(embedToken);
 
responseHeaders.set("Retry-After", reset.toString());
responseHeaders.set("X-RateLimit-Limit", limit.toString());
responseHeaders.set("X-RateLimit-Remaining", remaining.toString());
responseHeaders.set("X-RateLimit-Reset", reset.toString());
 
if (!success) {
  throw new DubApiError({
    code: "rate_limit_exceeded",
    message: "Too many requests.",
  });
}
 

Sources: apps/web/lib/embed/referrals/auth.ts:67-83

Rate limiting is enforced on every verified token using Upstash Redis with a fixed window of 60 requests per minute. Standard rate-limit metadata headers are injected into the response collection, including Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. If the limit is exceeded, a rate_limit_exceeded error is thrown.

Database Resolution and Context Injection

const { program, links, partnerGroup, ...programEnrollment } =
  await prisma.programEnrollment.findUniqueOrThrow({
    where: {
      partnerId_programId: {
        partnerId,
        programId,
      },
    },
    include: {
      links: {
        orderBy: [
          {
            saleAmount: "desc",
          },
          {
            leads: "desc",
          },
          {
            clicks: "desc",
          },
        ],
      },
      program: true,
      partnerGroup: true,
    },
  });
 
if (!partnerGroup) {
  throw new DubApiError({
    code: "forbidden",
    message:
      "You’re not part of any group yet. Please reach out to the program owner to be added.",
  });
}
 
return await handler({
  req,
  params,
  searchParams,
  program,
  programEnrollment,
  group: partnerGroup as PartnerGroupProps,
  links,
  embedToken,
});

Sources: apps/web/lib/embed/referrals/auth.ts:84-128

Once rate limits and token validity are confirmed, the middleware queries Prisma to locate the corresponding ProgramEnrollment record using a composite unique identifier combining partnerId and programId. The query includes relations for links (ordered descending by sale amount, leads, then clicks), program, and partnerGroup.

Relation IncludedOrder Criteria / FieldsPurpose
linkssaleAmount: desc, leads: desc, clicks: descRetrieves partner referral links sorted by performance
programFull relationAttaches parent referral program configuration
partnerGroupFull relationValidates group assignment and partner tier settings

Caution

If the partnerGroup relation resolves to null on the enrollment record, the wrapper throws a forbidden error stating that the user is not part of any group yet and must reach out to the program owner.

// GET /api/embed/referrals/token - get the referrals embed token
export const GET = withReferralsEmbedToken(async ({ embedToken }) => {
  return NextResponse.json(embedToken);
});

Sources: apps/web/app/ee/api/embed/referrals/token/route.ts:4-7

Target handlers such as the GET route at /api/embed/referrals/token receive the fully populated context object containing req, params, searchParams, program, programEnrollment, group, links, and embedToken.

Edge Request Path Rewriting

import { NextRequest, NextResponse } from "next/server";
import { parse } from "./utils/parse";
 
export function EmbedMiddleware(req: NextRequest) {
  const { path, searchParamsObj, fullPath } = parse(req);
 
  if (path.startsWith("/embed/support-chat")) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }
 
  if (searchParamsObj.token) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }
 
  // TODO: Show token expiry page
  return NextResponse.redirect(new URL("/", req.url));
}

Sources: apps/web/lib/middleware/embed.ts:1-17

At the edge network layer, EmbedMiddleware inspects incoming NextRequest paths and parameters. If the path starts with /embed/support-chat or if a query parameter token is present on the request, it rewrites the destination URL to point to app.dub.co combined with the full requested path. Otherwise, requests fall through to a default redirect to the root URL.

Client Embedding Library

React Integration and Component Architecture

"use client";
 
import { DubEmbedOptions, init } from "@dub/embed-core";
import { HTMLProps, memo, useEffect, useId, useRef } from "react";
 
type Options = Omit<DubEmbedOptions, "token">;
 
type DubEmbedProps = {
  token: DubEmbedOptions["token"];
  data: DubEmbedOptions["data"];
  options?: Options;
} & HTMLProps<HTMLDivElement>;
 
export const DubEmbed = memo(
  ({ token, data, options, ...rest }: DubEmbedProps) => (
    <DubEmbedInner options={{ ...options, token, data }} {...rest} />
  ),
);

Sources: packages/embeds/react/src/embed.tsx:1-18

The @dub/embed-react package exposes the DubEmbed component, which wraps client-side embedding initialization for React parent applications. The component accepts token, data, and optional configuration properties alongside standard HTMLDivElement HTML attributes. It utilizes React.memo to prevent unnecessary re-renders when parent states change without modifying embed parameters.

Prop NameTypePurpose
tokenDubEmbedOptions["token"]Public authentication token minted for the embed session
dataDubEmbedOptions["data"]Dataset identifier (e.g., "referrals") loaded by the widget
optionsOptionsOptional overrides excluding the token property

Note

DubEmbed components must be rendered inside client-side components designated with the "use client" directive, as they rely on React hooks and DOM ref attachments.

Lifecycle and DOM Initialization

function DubEmbedInner({
  options,
  ...rest
}: { options: DubEmbedOptions } & HTMLProps<HTMLDivElement>) {
  const id = useId();
  const rootRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    if (!rootRef.current) return;
 
    const { destroy } =
      init({
        root: rootRef.current,
        ...options,
      }) || {};
 
    return () => destroy?.();
  }, [id, JSON.stringify(options)]);
 
  return <div {...rest} ref={rootRef} />;
}

Sources: packages/embeds/react/src/embed.tsx:20-40

The inner component DubEmbedInner manages the lifecycle of the underlying core embedding script via a local useRef pointing to a container div. It invokes the init method imported from @dub/embed-core, passing the root DOM element and merged options.

Sources: packages/embeds/react/src/embed.tsx:20-40

Warning

The useEffect hook depends on JSON.stringify(options) and id. Modifying configuration objects inline without memoization in the parent application can trigger repeated destruction and re-initialization cycles of the core embed instance.

Parent Application Consumption

"use client";
 
import LayoutLoader from "@/ui/layout/layout-loader";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
import { DubEmbed } from "@dub/embed-react";
import { CursorRays, Hyperlink, InvoiceDollar, UserCheck } from "@dub/ui/icons";
import { fetcher } from "@dub/utils";
import { useSession } from "next-auth/react";
import useSWRImmutable from "swr/immutable";
 
export function ReferralsPageClient() {
  const { data: session, status } = useSession();
 
  const { data: { publicToken } = {}, isLoading } = useSWRImmutable<{
    publicToken: string;
  }>(session && "/api/user/referrals-token", fetcher, {
    keepPreviousData: true,
  });
 
  if (status === "loading" || isLoading) {
    return <LayoutLoader />;
  }
 
  if (!publicToken) {
    return (
      <div className="p-10">
        <AnimatedEmptyState
          title="Refer a friend"
          description="Activate your referral link to share the word about Dub and earn cash rewards"
          className="border-none"
          cardContent={
            <>
              <Hyperlink className="size-4 text-neutral-700" />
              <div className="h-2.5 w-24 min-w-0 rounded-sm bg-neutral-200" />
              <div className="xs:flex hidden grow items-center justify-end gap-1.5 text-neutral-500">
                <CursorRays className="size-3.5" />
                <UserCheck className="size-3.5" />
                <InvoiceDollar className="size-3.5" />
              </div>
            </>
          }
          pillContent="Coming soon"
        />
      </div>
    );
  }
 
  return <DubEmbed data="referrals" token={publicToken} />;
}

Sources: apps/web/app/app.dub.co/dashboard/account/settings/referrals/page-client.tsx:1-49

In dashboard pages such as the referrals settings view, client implementations fetch user session data and retrieve public embed tokens asynchronously using SWR immutable hooks against /api/user/referrals-token. While loading, components render layout loaders. If no token is provisioned, they present an empty state UI. Once the token resolves, DubEmbed renders with data="referrals" and the active publicToken.

Embedded Dashboard Data Resolution

Server-Side Data Retrieval and Enrollment Resolution

export const getReferralsEmbedData = async (token: string) => {
  const { programId, partnerId } = (await referralsEmbedToken.get(token)) ?? {};
 
  if (!programId || !partnerId) {
    notFound();
  }

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:14-19

The server action getReferralsEmbedData initiates embedded page initialization by extracting the token and retrieving associated metadata from storage via referralsEmbedToken.get(token). If either programId or partnerId is missing from the stored record, the function immediately triggers a notFound() response.

const programEnrollment = await getProgramEnrollmentOrThrow({
  partnerId,
  programId,
  include: {
    partner: {
      select: {
        id: true,
        name: true,
        email: true,
        username: true,
        country: true,
        tremendousEmail: true,
        defaultPayoutMethod: true,
        platforms: {
          select: {
            type: true,
            identifier: true,
            verifiedAt: true,
          },
        },
      },
    },
    program: {
      select: {
        id: true,
        name: true,
        slug: true,
        domain: true,
        defaultGroupId: true,
        minPayoutAmount: true,
        termsUrl: true,
        embedData: true,
        resources: true,
      },
    },
    links: {
      include: {
        linkReward: {
          include: {
            clickReward: true,
            leadReward: true,
            saleReward: true,
            discount: true,
          },
        },
      },
    },
    partnerGroup: true,
    clickReward: true,
    leadReward: true,
    saleReward: true,
    referralReward: true,
    customReward: true,
    discount: true,
    programPartnerTags: {
      select: {
        partnerTagId: true,
      },
    },
  },
});
 
if (!programEnrollment || !programEnrollment.partnerGroup) {
  notFound();
}

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:21-85

Once the identifiers are resolved, getProgramEnrollmentOrThrow queries the database for the active partner program enrollment. The Prisma query requests a selection of partner properties, program definitions, partner groups, reward configurations, and associated links. If the enrollment record or its assigned partnerGroup cannot be located, a notFound() exception is thrown.

Relation / FieldSelected SubfieldsPurpose
partnerid, name, email, username, country, tremendousEmail, defaultPayoutMethod, platformsIdentifies affiliate profile and payout preferences
programid, name, slug, domain, defaultGroupId, minPayoutAmount, termsUrl, embedData, resourcesConfigures affiliate program rules and branding resources
linkslinkReward (with clickReward, leadReward, saleReward, discount)Resolves custom link-level commission structures
partnerGroupAll fields (partnerGroup: true)Applies tier-specific group settings and limits
commissionsstatus, earnings (grouped)Aggregates earnings statistics by commission lifecycle status

Important

The database query explicitly includes clickReward, leadReward, saleReward, referralReward, customReward, and discount on the enrollment object to resolve fallback rewards for links lacking individual overrides.

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:21-85

Aggregation and Parallelized Prisma Queries

const { totalClicks, totalLeads, totalConversions } =
  aggregatePartnerLinksStats(links);
 
const [commissions, bounties] = await Promise.all([
  prisma.commission.groupBy({
    by: ["status"],
    _sum: {
      earnings: true,
    },
    _count: {
      id: true,
    },
    where: {
      earnings: {
        gt: 0,
      },
      programId,
      partnerId,
    },
  }),
 
  getBountiesForPartner(programEnrollment),
]);

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:100-122

Following program enrollment retrieval, link statistics are aggregated using aggregatePartnerLinksStats(links). Simultaneously, Promise.all executes two parallel database requests: a Prisma groupBy query on the commission table filtered by programId and partnerId where earnings > 0, and a call to getBountiesForPartner(programEnrollment).

earnings: {
  upcoming: commissions.reduce((acc, c) => {
    if (c.status === "pending" || c.status === "processed") {
      return acc + (c._sum.earnings ?? 0);
    }
    return acc;
  }, 0),
  paid: commissions.find((c) => c.status === "paid")?._sum.earnings ?? 0,
  totalCount: commissions.reduce((acc, c) => acc + c._count.id, 0),
},

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:155-164

The raw commission groups are reduced into summary totals for the dashboard UI. Upcoming earnings accumulate sums where status is either "pending" or "processed", paid earnings resolve from records matching the "paid" status, and total count aggregates all commission record IDs across groups.

Warning

Commission filtering requires earnings: { gt: 0 }. Zero-amount or negative commission entries are omitted from partner earnings totals during aggregation.

Sources: apps/web/app/ee/app.dub.co/embed/referrals/get-referrals-embed-data.ts:103-119

Page Routing and Suspense Fallback Rendering

export default async function ReferralsEmbedPage(props: {
  searchParams: Promise<{
    token: string;
    themeOptions?: string;
    dynamicHeight?: string;
  }>;
}) {
  const searchParams = await props.searchParams;
  const {
    token,
    themeOptions: themeOptionsRaw,
    dynamicHeight: dynamicHeightRaw,
  } = searchParams;
 
  const themeOptions = parseThemeOptions(themeOptionsRaw);
  const dynamicHeight = !!dynamicHeightRaw && dynamicHeightRaw !== "false";
 
  return (
    <>
      <Suspense fallback={<EmbedInlineLoading themeOptions={themeOptions} />}>
        <ReferralsEmbedRSC
          token={token}
          themeOptions={themeOptions}
          dynamicHeight={dynamicHeight}
        />
      </Suspense>
      {dynamicHeight && <DynamicHeightMessenger />}
    </>
  );
}

Sources: apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:9-38

The top-level Next.js Server Component ReferralsEmbedPage extracts search parameters (token, themeOptions, and dynamicHeight) asynchronously. It parses theme configurations using parseThemeOptions and normalizes the dynamic height boolean flag. It then renders the asynchronous server component ReferralsEmbedRSC wrapped inside a Suspense boundary with an EmbedInlineLoading fallback, alongside a DynamicHeightMessenger if enabled.

async function ReferralsEmbedRSC({
  token,
  themeOptions,
  dynamicHeight,
}: {
  token: string;
  themeOptions: ThemeOptions;
  dynamicHeight: boolean;
}) {
  const embedData = await getReferralsEmbedData(token);
 
  return (
    <ReferralsEmbedPageClient
      {...embedData}
      themeOptions={themeOptions}
      dynamicHeight={dynamicHeight}
    />
  );
}

Sources: apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:40-58

ReferralsEmbedRSC awaits getReferralsEmbedData(token) to resolve the complete dataset before passing it down to ReferralsEmbedPageClient, combining the aggregated payload with client theme options and height constraints.

Note

The EmbedInlineLoading component constructs an inline skeleton matching the dimensions of the referral link widget and reward cards, utilizing the background color specified in the theme options.

Sources: apps/web/app/ee/app.dub.co/embed/referrals/page.tsx:60-96

Partner Referral Interactivity

// POST /api/embed/referrals/links – create links for a partner
export const POST = withReferralsEmbedToken(
  async ({ req, programEnrollment, program, links, group }) => {
    if (!ACTIVE_ENROLLMENT_STATUSES.includes(programEnrollment.status)) {
      throw new DubApiError({
        code: "forbidden",
        message:
          "You cannot create links in this program because your enrollment is not active.",
      });
    }
 
    const { url, key } = createPartnerLinkSchema
      .pick({ url: true, key: true })
      .parse(await parseRequestBody(req));
 
    if (!program.domain || !program.url) {
      throw new DubApiError({
        code: "bad_request",
        message:
          "This program needs a domain and URL set before creating a link.",
      });
    }
 
    if (links.length >= group.maxPartnerLinks) {
      throw new DubApiError({
        code: "bad_request",
        message: `You have reached the limit of ${group.maxPartnerLinks} program links.`,
      });
    }
 
    validatePartnerLinkUrl({ group, url });
 

Sources: apps/web/app/ee/api/embed/referrals/links/route.ts:25-56

The embedded partner link route processes link generation requests through the POST handler wrapped with withReferralsEmbedToken. It first validates that programEnrollment.status is included in ACTIVE_ENROLLMENT_STATUSES. The handler parses the request body for url and key using createPartnerLinkSchema, confirms that the program has both a domain and a url configured, and verifies that the existing link count does not exceed group.maxPartnerLinks. Finally, it invokes validatePartnerLinkUrl to check destination validity against group rules.

const [workspaceOwner, partnerGroup, partner] = await Promise.all([
  prisma.projectUsers.findFirst({
    where: {
      projectId: program.workspaceId,
      role: "owner",
    },
    orderBy: {
      createdAt: "desc",
    },
    include: {
      project: {
        select: {
          id: true,
          webhookEnabled: true,
        },
      },
    },
  }),
 
  prisma.partnerGroup.findUnique({
    where: {
      id: group.id,
    },
    include: {
      partnerGroupDefaultLinks: true,
      utmTemplate: true,
    },
  }),
 
  prisma.partner.findUnique({
    where: {
      id: programEnrollment.partnerId,
    },
    select: {
      name: true,
    },
  }),
]);
 
// shouldn't happen but just in case
if (!partnerGroup) {
  throw new DubApiError({
    code: "not_found",
    message: "This partner is not part of a partner group.",
  });
}
 
const linkUrl = url || partnerGroup.partnerGroupDefaultLinks[0].url;
 
const { link, error, code } = await processLink({
  payload: {
    key: key || undefined,
    url: linkUrl,
    domain: program.domain,
    programId: program.id,
    folderId: program.defaultFolderId,
    tenantId: programEnrollment.tenantId,
    partnerId: programEnrollment.partnerId,
    trackConversion: true,
  },
  workspace: {
    id: program.workspaceId,
    plan: "business",
    users: [{ role: "owner" }],
  },
  userId: workspaceOwner?.userId,
  skipFolderChecks: true, // can't be changed by the partner
  skipProgramChecks: true, // can't be changed by the partner
  skipExternalIdChecks: true, // can't be changed by the partner
});

Sources: apps/web/app/ee/api/embed/referrals/links/route.ts:57-126

Once validated, the route queries the workspace owner user, partner group details including default links and UTM templates, and the partner name in parallel. It selects a default destination URL if none is supplied by falling back to partnerGroup.partnerGroupDefaultLinks[0].url. It then calls processLink with flags set to skip folder, program, and external ID checks, associating conversion tracking and tenancy identifiers.

Warning

If a partner's program enrollment status is inactive or if they exceed their tier's maxPartnerLinks limit, the API immediately throws a DubApiError with code forbidden or bad_request.

Sources: apps/web/app/ee/api/embed/referrals/links/route.ts:28-53

Leaderboard Presentation and Data SWR

export function ReferralsEmbedLeaderboard() {
  const token = useEmbedToken();
 
  const { data: partners, isLoading } = useSWR<
    z.infer<typeof LeaderboardPartnerSchema>[]
  >(
    "/api/embed/referrals/leaderboard",
    (url) =>
      fetcher(url, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }),
    {
      keepPreviousData: true,
    },
  );

Sources: apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:20-36

The ReferralsEmbedLeaderboard component fetches ranked partner performance via SWR from the /api/embed/referrals/leaderboard endpoint using an authorization bearer token. It maintains previous data during revalidation and parses rows against LeaderboardPartnerSchema.

const { table, ...tableProps } = useTable({
  data: partners || [],
  loading: isLoading,
  columns: [
    {
      id: "rank",
      header: "Rank",
      size: 40,
      minSize: 40,
      cell: ({ row }) => {
        return (
          <div className="flex w-8 items-center justify-start gap-1 tabular-nums">
            {row.index + 1}
            {row.index <= 2 && (
              <Crown
                className={cn("size-4", {
                  "text-amber-400": row.index === 0,
                  "text-neutral-400": row.index === 1,
                  "text-yellow-900": row.index === 2,
                })}
              />
            )}
          </div>
        );
      },
    },
    {
      id: "name",
      header: "Partner",
      cell: ({ row }) => {
        return (
          <div className="flex items-center gap-2">
            <PartnerAvatar partner={row.original} className="size-5" />
            <Tooltip content="For privacy reasons, the name of the partner is anonymized.">
              <span className="cursor-help text-sm font-medium decoration-dotted underline-offset-2 hover:underline">
                {generateRandomName(row.original.id)}
              </span>
            </Tooltip>
          </div>
        );
      },
    },
    {
      id: "totalCommissions",
      header: "Earnings",
      cell: ({ row }) => {
        return currencyFormatter(row.original.totalCommissions);
      },
    },
  ],
  emptyState: (
    <AnimatedEmptyState
      title="No partners found"
      description="No partners have been added to this program yet."
      cardContent={() => (
        <>
          <Users className="text-content-default size-4" />
          <div className="bg-bg-emphasis h-2.5 w-24 min-w-0 rounded-sm" />
        </>
      )}
      className="border-none md:min-h-fit"
    />
  ),
  thClassName: "border-l-0",
  tdClassName: "border-l-0",
  resourceName: (plural) => `partner${plural ? "s" : ""}`,
});

Sources: apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:38-104

The table UI configures three explicit columns: rank, partner name, and total commissions earnings. The rank cell displays numeric ranking alongside a Crown icon styled in amber, neutral, or yellow-brown for the top three positions. The partner column renders a PartnerAvatar alongside an anonymized partner name generated via generateRandomName(row.original.id) wrapped in a privacy tooltip. Earnings are formatted using currencyFormatter.

Note

When no partners populate the leaderboard, the table renders an AnimatedEmptyState component featuring a users icon and placeholder indicator lines.

Sources: apps/web/app/ee/app.dub.co/embed/referrals/leaderboard.tsx:88-100

Performance and Earnings Telemetry

Activity Metrics and Timeseries Telemetry

const querySchema = partnerProfileAnalyticsQuerySchema.pick({
  event: true,
  start: true,
  end: true,
  groupBy: true,
  saleType: true,
  interval: true,
});
 
// GET /api/embed/referrals/analytics – get analytics for a partner
export const GET = withReferralsEmbedToken(
  async ({ links, program, searchParams }) => {
    if (links.length === 0) {
      return NextResponse.json([]);
    }
 
    const parsedQuery = querySchema.parse(searchParams);
 
    const analytics = await getAnalytics({
      ...parsedQuery,
      workspaceId: program.workspaceId,
      linkId: parseFilterValue(links.map((link) => link.id)),
      dataAvailableFrom: program.startedAt ?? program.createdAt,
    });
 
    return NextResponse.json(analytics);
  },

Sources: apps/web/app/ee/api/embed/referrals/analytics/route.ts:7-33

The embedded analytics route handles retrieval of performance metrics via the GET method wrapped in withReferralsEmbedToken. It parses query parameters using partnerProfileAnalyticsQuerySchema restricted to event, start, end, groupBy, saleType, and interval. If the partner has no associated links, the endpoint returns an empty array. Otherwise, it calls getAnalytics with the workspace identifier, parsed filter values, and data availability boundaries derived from program.startedAt or program.createdAt.

export function ReferralsEmbedActivity() {
  const {
    group: { brandColor: color },
    stats: { clicks, leads, conversions },
  } = useReferralsEmbedData();
 
  const token = useEmbedToken();
 
  const isEmpty = clicks === 0 && leads === 0 && conversions === 0;
 
  const analyticsSearchParams = new URLSearchParams({
    event: "composite",
    groupBy: "timeseries",
    interval: "1y",
    saleType: "new",
  });
 
  const { data: analytics } = useSWR<AnalyticsTimeseries[]>(
    !isEmpty &&
      `/api/embed/referrals/analytics?${analyticsSearchParams.toString()}`,
    (url) =>
      fetcher(url, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }),
    {
      keepPreviousData: true,
      dedupingInterval: 60000,
    },
  );

Sources: apps/web/app/ee/app.dub.co/embed/referrals/activity.tsx:10-40

The ReferralsEmbedActivity component renders performance cards for clicks, leads, and conversions. It queries /api/embed/referrals/analytics using SWR with a composite event type, yearly interval, and new sale type. When click, lead, and conversion counts are all zero, it renders an EmptyState component featuring a CursorRays icon and a decorative SVG background gradient.

Note

Customer email addresses returned by event endpoints are dynamically obfuscated via obfuscateCustomerEmail unless customerDataSharingEnabledAt is configured on the program enrollment.

Sources: apps/web/app/ee/api/embed/referrals/events/route.ts:39-68

Commission Earnings and Payout Data

const querySchema = getPartnerEarningsQuerySchema
  .pick({
    start: true,
    end: true,
    page: true,
    pageSize: true,
    interval: true,
    timezone: true,
  })
  .extend({
    withTotal: z
      .string()
      .optional()
      .transform((v) => v?.toLowerCase() === "true")
      .default(false),
  });
 
// GET /api/embed/referrals/earnings – get commissions for a partner from an embed token
export const GET = withReferralsEmbedToken(
  async ({ programEnrollment, searchParams }) => {
    const { withTotal, start, end, pageSize, page, interval, timezone } =
      querySchema.parse(searchParams);
 
    const [earnings, total] = await Promise.all([
      getEarningsForPartner({
        page,
        
Was this page helpful?