Skip to main content
doc0blade47/dubdocumentation home
Sign in

QR Code Generator

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The QR code generator infrastructure powers dynamic matrix rendering, custom geometry styling, and multi-format client or server delivery across link and partner workflows. Utilizing a robust TypeScript matrix generation core combined with edge-rendered endpoints, the system transforms short links and URLs into high-performance vector and raster graphics with integrated branding.

Sources: apps/web/lib/qr/codegen.ts:1-32, apps/web/app/api/qr/route.tsx:18-58

Low-level Matrix Generation

Binary Encoding and Segmentation

The generator maps input text or binary payloads into a structured sequence of data codeword bytes via QrCode.encodeSegments(). It iterates through versions from minVersion to maxVersion (1 to 40) to find the minimal version where usedBits fits within dataCapacityBits. If boostEcl is enabled, the error correction level is upgraded from LOW to MEDIUM, QUARTILE, or HIGH if the data still fits. Segments prepend their mode bits and character count bits, followed by payload data, a terminator, padding up to a byte boundary, and alternating pad bytes (0xec and 0x11) until reaching the data capacity.

public static encodeSegments(
  segs: Readonly<Array<QrSegment>>,
  ecl: QrCode.Ecc,
  minVersion: int = 1,
  maxVersion: int = 40,
  mask: int = -1,
  boostEcl: boolean = true,
): QrCode {
  if (
    !(
      QrCode.MIN_VERSION <= minVersion &&
      minVersion <= maxVersion &&
      maxVersion <= QrCode.MAX_VERSION
    ) ||
    mask < -1 ||
    mask > 7
  )
    throw new RangeError("Invalid value");
 
  // Find the minimal version number to use
  let version: int;
  let dataUsedBits: int;
  for (version = minVersion; ; version++) {
    const dataCapacityBits: int =
      QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
    const usedBits: number = QrSegment.getTotalBits(segs, version);
    if (usedBits <= dataCapacityBits) {
      dataUsedBits = usedBits;
      break; // This version number is found to be suitable
    }
    if (version >= maxVersion)
      // All versions in the range could not fit the given data
      throw new RangeError("Data too long");
  }
 
  // Increase the error correction level while the data still fits in the current version number
  for (const newEcl of [
    QrCode.Ecc.MEDIUM,
    QrCode.Ecc.QUARTILE,
    QrCode.Ecc.HIGH,
  ]) {
    // From low to high
    if (
      boostEcl &&
      dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8
    )
      ecl = newEcl;
  }
 
  // Concatenate all segments to create the data bit string
  let bb: Array<bit> = [];
  for (const seg of segs) {
    appendBits(seg.mode.modeBits, 4, bb);
    appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
    for (const b of seg.getData()) bb.push(b);
  }
  assert(bb.length == dataUsedBits);
 
  // Add terminator and pad up to a byte if applicable
  const dataCapacityBits: int =
    QrCode.getNumDataCodewords(version, ecl) * 8;
  assert(bb.length <= dataCapacityBits);
  appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
  appendBits(0, (8 - (bb.length % 8)) % 8, bb);
  assert(bb.length % 8 == 0);
 
  // Pad with alternating bytes until data capacity is reached
  for (
    let padByte = 0xec;
    bb.length < dataCapacityBits;
    padByte ^= 0xec ^ 0x11
  )
    appendBits(padByte, 8, bb);
 
  // Pack bits into bytes in big endian
  let dataCodewords: Array<byte> = [];
  while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
  bb.forEach(
    (b: bit, i: int) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7))),
  );

Sources: apps/web/lib/qr/codegen.ts:68-151

Note

Error correction level boosting (boostEcl) automatically upgrades the ECC level without increasing the QR code version if the payload capacity permits.

Matrix Coordinate Computation and Masking

The QrCode constructor initializes square grid arrays of size version * 4 + 17 (ranging from 21 to 177 modules). It draws function patterns, appends Reed-Solomon error correction codes, interleaves codewords, and draws the resulting modules onto the grid. When mask selection is automatic (msk == -1), the constructor iterates through all 8 mask patterns (0 to 7), applies each mask, draws the format bits, calculates a penalty score, and selects the mask yielding the lowest penalty.

// The width and height of this QR Code, measured in modules, between
// 21 and 177 (inclusive). This is equal to version * 4 + 17.
public readonly size: int;
 
// The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
// Even if a QR Code is created with automatic masking requested (mask = -1),
// the resulting object still has a mask value between 0 and 7.
public readonly mask: int;
 
// The modules of this QR Code (false = light, true = dark).
// Immutable after constructor finishes. Accessed through getModule().
private readonly modules: Array<Array<boolean>> = [];
 
// Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
private readonly isFunction: Array<Array<boolean>> = [];
 
/*-- Constructor (low level) and fields --*/
 
// Creates a new QR Code with the given version number,
// error correction level, data codeword bytes, and mask number.
// This is a low-level API that most users should not use directly.
// A mid-level API is the encodeSegments() function.
public constructor(
  // The version number of this QR Code, which is between 1 and 40 (inclusive).
  // This determines the size of this barcode.
  public readonly version: int,
 
  // The error correction level used in this QR Code.
  public readonly errorCorrectionLevel: QrCode.Ecc,
 
  dataCodewords: Readonly<Array<byte>>,
 
  msk: int,
) {
  // Check scalar arguments
  if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
    throw new RangeError("Version value out of range");
  if (msk < -1 || msk > 7) throw new RangeError("Mask value out of range");
  this.size = version * 4 + 17;
 
  // Initialize both grids to be size*size arrays of Boolean false
  let row: Array<boolean> = [];
  for (let i = 0; i < this.size; i++) row.push(false);
  for (let i = 0; i < this.size; i++) {
    this.modules.push(row.slice()); // Initially all light
    this.isFunction.push(row.slice());
  }
 
  // Compute ECC, draw modules
  this.drawFunctionPatterns();
  const allCodewords: Array<byte> = this.addEccAndInterleave(dataCodewords);
  this.drawCodewords(allCodewords);
 
  // Do masking
  if (msk == -1) {
    // Automatically choose best mask
    let minPenalty: int = 1000000000;
    for (let i = 0; i < 8; i++) {
      this.applyMask(i);
      this.drawFormatBits(i);
      const penalty: int = this.getPenaltyScore();
      if (penalty < minPenalty) {
        msk = i;
        minPenalty = penalty;
      }

Sources: apps/web/lib/qr/codegen.ts:155-219

Configuration and Error Levels

Error correction levels are mapped from standard single-character identifiers to internal QrCode.Ecc enum values via ERROR_LEVEL_MAP, supporting low, medium, quartile, and high error resilience.

Level KeyInternal Ecc ConstantError Correction Capability
Lqrcodegen.QrCode.Ecc.LOWRecovers ~7% of data
Mqrcodegen.QrCode.Ecc.MEDIUMRecovers ~15% of data
Qqrcodegen.QrCode.Ecc.QUARTILERecovers ~25% of data
Hqrcodegen.QrCode.Ecc.HIGHRecovers ~30% of data
export const ERROR_LEVEL_MAP: { [index: string]: qrcodegen.QrCode.Ecc } = {
  L: qrcodegen.QrCode.Ecc.LOW,
  M: qrcodegen.QrCode.Ecc.MEDIUM,
  Q: qrcodegen.QrCode.Ecc.QUARTILE,
  H: qrcodegen.QrCode.Ecc.HIGH,
};

Sources: apps/web/lib/qr/constants.ts:3-8

Geometry and Module Customization

Module and Finder Customization Options

The Dub QR generator supports styling configurations for both data modules and finder patterns. Data modules can be rendered using three distinct styles specified by the DotStyle type, while finder patterns support separate border and center customizations via MarkerBorderStyle and MarkerCenterStyle.

Customization CategoryType NameSupported Values
Data ModulesDotStyle"square", "rounded", "extra-rounded"
Marker CentersMarkerCenterStyle"square", "circle"
Marker BordersMarkerBorderStyle"square", "rounded-square", "circle"
export type DotStyle = "square" | "rounded" | "extra-rounded";
export type MarkerCenterStyle = "square" | "circle";
export type MarkerBorderStyle = "square" | "rounded-square" | "circle";

Sources: apps/web/lib/qr/types.ts:7-9

Finder Pattern Path Generation

Finder patterns are rendered as 7×7 module zones at the corners of the QR matrix. The finderBorderPath function constructs compound SVG paths for the border ring using the evenodd fill rule. This ensures the internal gap remains transparent without requiring an opaque background rectangle, avoiding the hard-cornered white squares that clash with rounded or circular themes.

function finderBorderPath(
  x: number,
  y: number,
  borderStyle: MarkerBorderStyle,
): string {
  const cx = x + 3.5;
  const cy = y + 3.5;
 
  if (borderStyle === "square") {
    const outer = `M${x},${y}H${x + 7}V${y + 7}H${x}Z`;
    const inner = `M${x + 1},${y + 1}H${x + 6}V${y + 6}H${x + 1}Z`;
    return `${outer} ${inner}`;
  }
 
  if (borderStyle === "rounded-square") {
    const outer = roundedRectPath(x, y, 7, 7, 1.5);
    // Inner cutout uses a smaller radius so corners look intentional
    const inner = roundedRectPath(x + 1, y + 1, 5, 5, 0.75);
    return `${outer} ${inner}`;
  }
 
  // circle
  const outer = circlePath(cx, cy, 3.5);
  const inner = circlePath(cx, cy, 2.5);
  return `${outer} ${inner}`;
}

Sources: apps/web/lib/qr/utils.tsx:378-403

Note

When markerColor is omitted from the QR configuration, the generator falls back to using the primary foreground color (fgColor).

const effectiveMarkerColor = markerColor ?? fgColor;

Sources: apps/web/lib/qr/utils.tsx:471

Logo Excavation and Error Level Adjustment

When an image logo is embedded via ImageSettings, the getImageSettings function calculates bounding box dimensions and scale factors relative to the QR code size. If imageSettings.excavate is enabled, module grid coordinates underneath the logo are cleared to prevent visual distortion.

let excavation: Excavation | null = null;
if (imageSettings.excavate) {
  const floorX = Math.floor(x);
  const floorY = Math.floor(y);
  const ceilW = Math.ceil(w + x - floorX);
  const ceilH = Math.ceil(h + y - floorY);
  excavation = { x: floorX, y: floorY, w: ceilW, h: ceilH };
}

Sources: apps/web/lib/qr/utils.tsx:307-314

Warning

In Open Graph rendering contexts (isOGContext), if logo excavation is enabled and the error level is set to L or M, the generator automatically upgrades the effective error correction level to Q to compensate for the obscured data modules.

const shouldUseHigherErrorLevel =
  isOGContext && imageSettings?.excavate && (level === "L" || level === "M");
 
const effectiveLevel = shouldUseHigherErrorLevel ? "Q" : level;

Sources: apps/web/lib/qr/utils.tsx:473-476

Client-Side Rendering and Export

SVG Data URI Generation

The getQRAsSVGDataUri function encodes the input text into a QR matrix, applies margin settings, and constructs an inline Scalable Vector Graphics string wrapped as a data URI. When imageSettings are provided along with valid excavation boundaries, module coordinates underneath the logo are cleared, and an external image source is fetched, converted to base64 via getBase64Image, and embedded using an <image> element with preserveAspectRatio="none".

export async function getQRAsSVGDataUri(props: QRProps) {
  const {
    value,
    size = DEFAULT_SIZE,
    level = DEFAULT_LEVEL,
    bgColor = DEFAULT_BGCOLOR,
    fgColor = DEFAULT_FGCOLOR,
    margin = DEFAULT_MARGIN,
    imageSettings,
    dotStyle = "square",
    markerCenterStyle = "square",
    markerBorderStyle = "square",
    markerColor,
  } = props;
 
  const effectiveMarkerColor = markerColor ?? fgColor;
 
  let cells = qrcodegen.QrCode.encodeText(
    value,
    ERROR_LEVEL_MAP[level],
  ).getModules();
 
  const numCells = cells.length + margin * 2;
  const calculatedImageSettings = getImageSettings(
    cells,
    size,
    margin,
    imageSettings,
  );
 
  let image = "";
  if (imageSettings != null && calculatedImageSettings != null) {
    if (calculatedImageSettings.excavation != null)
      cells = excavateModules(cells, calculatedImageSettings.excavation);
 
    const base64Image = await getBase64Image(imageSettings.src);
 
    image = [
      `<image href="${base64Image}"`,
      `height="${calculatedImageSettings.h}"`,
      `width="${calculatedImageSettings.w}"`,
      `x="${calculatedImageSettings.x + margin}"`,
      `y="${calculatedImageSettings.y + margin}"`,
      'preserveAspectRatio="none"></image>',
    ].join(" ");
  }

Sources: apps/web/lib/qr/index.tsx:315-360

The final SVG document combines background rectangles, data dot paths generated via generateSquareDotPath, generateRoundedDotPath, or generateExtraRoundedDotPath, finder patterns mapped across the three corner positions, and the embedded logo. This string is then prefixed with data:image/svg+xml, and URL-encoded.

  const fgPath =
    dotStyle === "rounded"
      ? generateRoundedDotPath(cells, margin)
      : dotStyle === "extra-rounded"
        ? generateExtraRoundedDotPath(cells, margin)
        : generateSquareDotPath(cells, margin);
 
  const numModules = cells.length;
  const finderPositions = [
    { x: margin, y: margin },
    { x: numModules - 7 + margin, y: margin },
    { x: margin, y: numModules - 7 + margin },
  ];
 
  const finderSVG = finderPositions
    .map((pos) =>
      getFinderPatternSVGString({
        x: pos.x,
        y: pos.y,
        markerColor: effectiveMarkerColor,
        bgColor,
        borderStyle: markerBorderStyle,
        centerStyle: markerCenterStyle,
      }),
    )
    .join("");
 
  const svgData = [
    `<svg xmlns="http://www.w3.org/2000/svg" height="${size}" width="${size}" viewBox="0 0 ${numCells} ${numCells}">`,
    `<path fill="${bgColor}" d="M0,0 h${numCells}v${numCells}H0z" shape-rendering="crispEdges"></path>`,
    `<path fill="${fgColor}" d="${fgPath}" shape-rendering="crispEdges"></path>`,
    finderSVG,
    image,
    "</svg>",
  ].join("");
 
  return `data:image/svg+xml,${encodeURIComponent(svgData)}`;
}

Sources: apps/web/lib/qr/index.tsx:362-399

Note

The getBase64Image helper creates an offscreen Image instance with crossOrigin set to anonymous, draws it onto a temporary canvas, and extracts a PNG data URL using canvas.toDataURL("image/png").

const getBase64Image = (imgUrl: string) => {
  return new Promise(function (resolve, reject) {
    const img = new Image();
    img.src = imgUrl;
    img.setAttribute("crossOrigin", "anonymous");
 
    img.onload = function () {
      const canvas = document.createElement("canvas");
 
      canvas.width = img.width;
      canvas.height = img.height;
 
      const ctx = canvas.getContext("2d");
      ctx?.drawImage(img, 0, 0);
 
      const dataURL = canvas.toDataURL("image/png");
      resolve(dataURL);
    };
 
    img.onerror = function () {
      reject("The image could not be loaded.");
    };
  });
};

Sources: apps/web/lib/qr/index.tsx:401-424

Canvas Context Rendering

The getQRAsCanvas function renders the QR code onto an HTML canvas element. It initializes a canvas context, calculates pixel ratios using window.devicePixelRatio, scales the coordinate space to match module boundaries, and fills the background using bgColor.

export async function getQRAsCanvas(
  props: QRProps,
  type: string,
  getCanvas?: boolean,
): Promise<HTMLCanvasElement | string> {
  const {
    value,
    size = DEFAULT_SIZE,
    level = DEFAULT_LEVEL,
    bgColor = DEFAULT_BGCOLOR,
    fgColor = DEFAULT_FGCOLOR,
    margin = DEFAULT_MARGIN,
    imageSettings,
    dotStyle = "square",
    markerCenterStyle = "square",
    markerBorderStyle = "square",
    markerColor,
  } = props;
 
  const effectiveMarkerColor = markerColor ?? fgColor;
 
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
 
  let cells = qrcodegen.QrCode.encodeText(
    value,
    ERROR_LEVEL_MAP[level],
  ).getModules();
  const numCells = cells.length + margin * 2;
  const calculatedImageSettings = getImageSettings(
    cells,
    size,
    margin,
    imageSettings,
  );
 
  const image = new Image();
  image.crossOrigin = "anonymous";
  if (calculatedImageSettings) {
    // @ts-expect-error: imageSettings is not null
    await waitUntilImageLoaded(image, imageSettings.src);
    if (calculatedImageSettings.excavation != null) {
      cells = excavateModules(cells, calculatedImageSettings.excavation);
    }
  }
 
  const pixelRatio = window.devicePixelRatio || 1;
  canvas.height = canvas.width = size * pixelRatio;
  const scale = (size / numCells) * pixelRatio;
  ctx.scale(scale, scale);
 
  // Background
  ctx.fillStyle = bgColor;
  ctx.fillRect(0, 0, numCells, numCells);
 

Sources: apps/web/lib/qr/index.tsx:440-494

Data dots excluding finder pattern cells are drawn iteratively based on the selected dotStyle. For rounded and extra-rounded styles, helper checks evaluate neighboring cells to determine corner arc radii dynamically before executing path drawing routines.

// Data dots (excluding finder pattern cells)
ctx.fillStyle = fgColor;
if (dotStyle === "rounded") {
  const r = 0.4;
  cells.forEach((row, y) => {
    row.forEach((cell, x) => {
      if (!cell || isFinderPatternCell(x, y, cells.length)) return;
      const px = x + margin;
      const py = y + margin;
      drawRoundedRect(ctx, px, py, 1, 1, r);
    });
  });
} else if (dotStyle === "extra-rounded") {
  const r = 0.5;
  const n = cells.length;
  const isDark = (xi: number, yi: number) =>
    xi >= 0 &&
    yi >= 0 &&
    xi < n &&
    yi < n &&
    !!(cells[yi][xi] && !isFinderPatternCell(xi, yi, n));
 
  cells.forEach((row, y) => {
    row.forEach((cell, x) => {
      if (!cell || isFinderPatternCell(x, y, n)) return;
 
      const px = x + margin;
      const py = y + margin;
      const top = isDark(x, y - 1);
      const right = isDark(x + 1, y);
      const bottom = isDark(x, y + 1);
      const left = isDark(x - 1, y);
 
      // Draw as a rounded rect where connected sides have no arc
      const rTL = top || left ? 0 : r;
      const rTR = top || right ? 0 : r;
      const rBR = bottom || right ? 0 : r;
      const rBL = bottom || left ? 0 : r;
 
      ctx.beginPath();
      ctx.moveTo(px + rTL, py);
      ctx.lineTo(px + 1 - rTR, py);
      if (rTR > 0) ctx.arcTo(px + 1, py, px + 1, py + rTR, rTR);
      ctx.lineTo(px + 1, py + 1 - rBR);
      if (rBR > 0) ctx.arcTo(px + 1, py + 1, px + 1 - rBR, py + 1, rBR);
      ctx.lineTo(px + rBL, py + 1);

Sources: apps/web/lib/qr/index.tsx:495-540

Warning

When rendering images on the canvas, waitUntilImageLoaded assigns eager loading flags and waits for asynchronous image asset resolution before excavating modules and drawing the source image into the canvas context.

function waitUntilImageLoaded(img: HTMLImageElement, src: string) {
  return new Promise((resolve) => {
    function onFinish() {
      img.onload = null;
      img.onerror = null;
      resolve(true);
    }
    img.onload = onFinish;
    img.onerror = onFinish;
    img.src = src;
    img.loading = "eager";
  });
}

Sources: apps/web/lib/qr/index.tsx:426-438

React Memoization and Scale Wrapper

The memoized QRCode component wraps QRCodeSVG, invoking getQRData inside a useMemo hook with dependencies on the target URL, foreground color, logo visibility, scale factor, and dot styling options. It scales component dimensions and image settings relative to an eighth-scale factor before passing properties down to the rendering layer.

export const QRCode = memo(
  ({
    url,
    fgColor,
    hideLogo,
    logo,
    scale = 1,
    margin = DEFAULT_MARGIN,
    dotStyle,
    markerCenterStyle,
    markerBorderStyle,
    markerColor,
  }: {
    url: string;
    fgColor?: string;
    hideLogo?: boolean;
    logo?: string;
    scale?: number;
    margin?: number;
    dotStyle?: DotStyle;
    markerCenterStyle?: MarkerCenterStyle;
    markerBorderStyle?: MarkerBorderStyle;
    markerColor?: string;
  }) => {
    const qrData = useMemo(
      () =>
        getQRData({
          url,
          fgColor,
          hideLogo,
          logo,
          margin,
          dotStyle,
          markerCenterStyle,
          markerBorderStyle,
          markerColor,
        }),
      [
        url,
        fgColor,
        hideLogo,
        logo,
        margin,
        dotStyle,
        markerCenterStyle,
        markerBorderStyle,
        markerColor,
      ],
    );
 
    return (
      <QRCodeSVG
        value={qrData.value}
        size={(qrData.size / 8) * scale}
        bgColor={qrData.bgColor}
        fgColor={qrData.fgColor}
        level={qrData.level}
        margin={qrData.margin}
        dotStyle={qrData.dotStyle}
        markerCenterStyle={qrData.markerCenterStyle}
        markerBorderStyle={qrData.markerBorderStyle}
        markerColor={qrData.markerColor}
        {...(qrData.imageSettings && {
          imageSettings: {
            ...qrData.imageSettings,
            height: qrData.imageSettings
              ? (qrData.imageSettings.height / 8) * scale
              : 0,
            width: qrData.imageSettings
              ? (qrData.imageSettings.width / 8) * scale
              : 0,
          },
        })}
      />
    );
  },
);
 
QRCode.displayName = "QRCode";

Sources: apps/web/ui/shared/qr-code.tsx:6-84

Server Engine and Endpoint Delivery

Execution and Validation

The server-side QR generation endpoint is deployed on the Edge runtime (export const runtime = "edge") at the /qr path defined in qrCodePaths. Incoming requests to the GET handler are intercepted, CORS headers allowing GET and OPTIONS methods are appended via CORS_HEADERS, and the query parameters from the request URL are parsed and validated using the getQRCodeQuerySchema Zod object.

export const runtime = "edge";
 
const CORS_HEADERS = new Headers({
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
});
 
export async function GET(req: NextRequest) {
  try {
    const paramsParsed = getQRCodeQuerySchema.parse(getSearchParams(req.url));
 

Sources: apps/web/app/api/qr/route.tsx:11-21

export const qrCodePaths: ZodOpenApiPathsObject = {
  "/qr": {
    get: getQRCode,
  },
};

Sources: apps/web/lib/openapi/qr/index.ts:29-33

The Zod validation schema establishes strict defaults and types for query parameters. The parameters validated by getQRCodeQuerySchema include:

ParameterTypeDefaultDescription
urlstring (parsed URL)—The URL to generate a QR code for.
logostring (optional)undefinedThe logo to include in the QR code (restricted to paid plans).
sizenumber (coerced)600The size of the QR code in pixels.
levelenum (QR_LEVELS)"L"The error correction level (L, M, Q, H).
fgColorstring (optional)DEFAULT_FGCOLOR (#000000)The foreground color of the QR code in hex format.
bgColorstring (optional)DEFAULT_BGCOLOR (#ffffff)The background color of the QR code in hex format.
hideLogoboolean queryfalseWhether to hide the logo in the QR code (restricted to paid plans).
marginnumber (coerced)DEFAULT_MARGINThe size of the margin around the QR code.
includeMarginboolean querytrueDeprecated margin inclusion toggle.
export const getQRCodeQuerySchema = z.object({
  url: parseUrlSchema.describe("The URL to generate a QR code for."),
  logo: z
    .string()
    .optional()
    .describe(
      "The logo to include in the QR code. Can only be used with a paid plan on Dub.",
    ),
  size: z.coerce
    .number()
    .optional()
    .default(600)
    .describe(
      "The size of the QR code in pixels. Defaults to `600` if not provided.",
    ),
  level: z
    .enum(QR_LEVELS)
    .optional()
    .default("L")
    .describe(
      "The level of error correction to use for the QR code. Defaults to `L` if not provided.",
    ),
  fgColor: z
    .string()
    .optional()
    .default(DEFAULT_FGCOLOR)
    .describe(
      "The foreground color of the QR code in hex format. Defaults to `#000000` if not provided.",
    ),
  bgColor: z
    .string()
    .optional()
    .default(DEFAULT_BGCOLOR)
    .describe(
      "The background color of the QR code in hex format. Defaults to `#ffffff` if not provided.",
    ),
  hideLogo: booleanQuerySchema
    .optional()
    .default(false)
    .describe(
      "Whether to hide the logo in the QR code. Can only be used with a paid plan on Dub.",
    ),
  margin: z.coerce
    .number()
    .optional()
    .default(DEFAULT_MARGIN)
    .describe(
      `The size of the margin around the QR code. Defaults to ${DEFAULT_MARGIN} if not provided.`,
    ),
  includeMargin: booleanQuerySchema
    .optional()
    .default(true)
    .describe(
      "DEPRECATED: Margin is included by default. Use the `margin` prop to customize the margin size.",
    )
    .meta({ deprecated: true }),
});

Sources: apps/web/lib/zod/schemas/qr.ts:11-67

Warning

The includeMargin query parameter is marked as deprecated (.meta({ deprecated: true })), as margin size is included by default; callers should use the margin parameter instead.

includeMargin: booleanQuerySchema
  .optional()
  .default(true)
  .describe(
    "DEPRECATED: Margin is included by default. Use the `margin` prop to customize the margin size.",
  )
  .meta({ deprecated: true }),

Sources: apps/web/lib/zod/schemas/qr.ts:60-66

Rate Limiting and Image Response

Following parameter validation, the handler enforces request throttling by invoking ratelimitOrThrow(req, "qr"). If successful, the endpoint resolves the appropriate logo via getQRCodeLogo and constructs a Next.js ImageResponse wrapping the SVG output from QRCodeSVG. The response configures the specified dimensions, header rules, and excavation settings where logo image dimensions are set to one-quarter of the overall code size (size / 4).

await ratelimitOrThrow(req, "qr");
 
const { logo, url, size, level, fgColor, bgColor, margin, hideLogo } =
  paramsParsed;
 
const qrCodeLogo = await getQRCodeLogo({ url, logo, hideLogo });
 
return new ImageResponse(
  QRCodeSVG({
    value: url,
    size,
    level,
    fgColor,
    bgColor,
    margin,
    ...(qrCodeLogo
      ? {
          imageSettings: {
            src: qrCodeLogo,
            height: size / 4,
            width: size / 4,
            excavate: true,
          },
        }
      : {}),
    isOGContext: true,
  }),
  {
    width: size,
    height: size,
    headers: CORS_HEADERS,
  },
);

Sources: apps/web/app/api/qr/route.tsx:22-54

export const getQRCode: ZodOpenApiOperationObject = {
  operationId: "getQRCode",
  "x-speakeasy-name-override": "get",
  summary: "Retrieve a QR code",
  description: "Retrieve a QR code for a link.",
  requestParams: {
    query: getQRCodeQuerySchema,
  },
  responses: {
    "200": {
      description: "The QR code",
      content: {
        "image/png": {
          schema: z.string(),
        },
      },
    },
    ...openApiErrorResponses,
  },
  tags: ["QR Codes"],
};

Sources: apps/web/lib/openapi/qr/index.ts:7-27

Note

Preflight OPTIONS requests are handled explicitly by returning an empty Response with status 204 and the shared CORS_HEADERS.

export function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: CORS_HEADERS,
  });
}

Sources: apps/web/app/api/qr/route.tsx:106-111

Dynamic Logo Resolution via Edge DB

The getQRCodeLogo function resolves the graphic asset embedded in the center of the QR code by querying edge databases (getShortLinkViaEdge, getWorkspaceViaEdge, and getDomainViaEdge). It follows a strict fallback sequence based on link ownership, workspace subscription plans, and custom domain configurations.

const getQRCodeLogo = async ({
  url,
  logo,
  hideLogo,
}: {
  url: string;
  logo: string | undefined;
  hideLogo: boolean;
}) => {
  const shortLink = await getShortLinkViaEdge(url.split("?")[0]);

Sources: apps/web/app/api/qr/route.tsx:60-69

The resolution logic evaluates conditions sequentially:

  1. If the target URL does not match a short link (!shortLink), it returns the default Dub QR logo (DUB_QR_LOGO).
  2. If the associated workspace plan is "free", it returns DUB_QR_LOGO.
  3. If hideLogo is set to true, it returns null.
  4. If an explicit logo parameter is passed, it returns that logo string.
  5. If the link uses a Dub-owned domain (isDubDomain(shortLink.domain)) and the workspace has no custom logo set, it returns DUB_QR_LOGO.
  6. For custom domains, it queries domain-level metadata via getDomainViaEdge(shortLink.domain) and returns domain.logo, falling back to workspace.logo, and ultimately defaulting to DUB_QR_LOGO.
 
  // Not a Dub link
  if (!shortLink) {
    return DUB_QR_LOGO;
  }
 
  const workspace = await getWorkspaceViaEdge({
    workspaceId: shortLink.projectId,
  });
 
  if (workspace?.plan === "free") {
    return DUB_QR_LOGO;
  }
 
  // if hideLogo is set, return null
  if (hideLogo) {
    return null;
  }
 
  // if logo is passed, return it
  if (logo) {
    return logo;
  }
 
  // if it's a Dub owned domain and no  workspace logo is set, use the Dub logo
  if (isDubDomain(shortLink.domain) && !workspace?.logo) {
    return DUB_QR_LOGO;
  }
 
  // if it's a custom domain, check if it has a logo
  const domain = await getDomainViaEdge(shortLink.domain);
 
  // return domain logo if it has one, otherwise fallback to workspace logo, and finally fallback to Dub logo
  return domain?.logo || workspace?.logo || DUB_QR_LOGO;
};

Sources: apps/web/app/api/qr/route.tsx:70-104

Interactive Design Modals

The QR code design interface is structured around shared form components that handle customization state, live previews, and action buttons across both link configuration dialogs and partner program enrollment views. The layout centers on QRCodeDesignFields, which renders a live preview container alongside form controls for dot styles, marker centers, marker borders, foreground colors, and optional logo visibility slots.

export function QRCodeDesignFields({
  data,
  setData,
  url,
  logo,
  linkProps,
  qrData,
  onClose,
  logoSection,
}: {
  data: QRCodeDesign;
  setData: Dispatch<SetStateAction<QRCodeDesign>>;
  url?: string;
  logo?: string;
  linkProps: QRLinkProps;
  qrData: ReturnType<typeof getQRData> | null;
  onClose: () => void;
  /** Optional slot rendered left of Dot style (link modal uses this for Logo toggle). */
  logoSection?: React.ReactNode;
}) {

Sources: apps/web/ui/modals/qr-code-design-fields.tsx:60-79

The primary modal implementations wrap these fields inside unmanaged or locally persisted state flows:

  • LinkQRModal: Manages user settings within workspace local storage under the key qr-code-design-${workspaceId} with a default configuration defined by DEFAULT_QR_DESIGN. It exposes keyboard shortcut integration via useLinkBuilderKeyboardShortcut bound to the Q key.
  • PartnerLinkQRModal: Scopes persisted design state to partner programs via local storage key qr-code-design-program-${programEnrollment?.program?.id}.
function LinkQRModal(
  props: {
    showLinkQRModal: boolean;
    setShowLinkQRModal: Dispatch<SetStateAction<boolean>>;
  } & LinkQRModalProps,
) {
  return (
    <Modal
      showModal={props.showLinkQRModal}
      setShowModal={props.setShowLinkQRModal}
      className="max-w-[500px]"
    >
      <LinkQRModalInner {...props} />
    </Modal>
  );
}
 
function LinkQRModalInner({
  props,
  onSave,
  showLinkQRModal,
  setShowLinkQRModal,
}: {
  showLinkQRModal: boolean;
  setShowLinkQRModal: Dispatch<SetStateAction<boolean>>;
} & LinkQRModalProps) {
  const { id: workspaceId, slug, plan, logo: workspaceLogo } = useWorkspace();
  const { logo: domainLogo } = useDomain({
    slug: props.domain,
    enabled: showLinkQRModal,
  });
 
  const isPro = plan && plan !== "free";
 
  const url = useMemo(
    () =>
      props.key && props.domain
        ? linkConstructor({ key: props.key, domain: props.domain })
        : undefined,
    [props.key, props.domain],
  );
 
  const [dataPersisted, setDataPersisted] = useLocalStorage<QRCodeDesign>(
    `qr-code-design-${workspaceId}`,
    DEFAULT_QR_DESIGN,
  );
 
  const [data, setData] = useState<QRCodeDesign>(dataPersisted);
 

Sources: apps/web/ui/modals/link-qr-modal.tsx:37-85

function PartnerLinkQRModal(
  props: {
    showLinkQRModal: boolean;
    setShowLinkQRModal: Dispatch<SetStateAction<boolean>>;
  } & PartnerLinkQRModalProps,
) {
  return (
    <Modal
      showModal={props.showLinkQRModal}
      setShowModal={props.setShowLinkQRModal}
      className="max-w-[500px]"
    >
      <PartnerLinkQRModalInner {...props} />
    </Modal>
  );
}
 
function PartnerLinkQRModalInner({
  props,
  onSave,
  setShowLinkQRModal,
}: {
  showLinkQRModal: boolean;
  setShowLinkQRModal: Dispatch<SetStateAction<boolean>>;
} & PartnerLinkQRModalProps) {
  const { programEnrollment } = useProgramEnrollment();
  const { logo } = programEnrollment?.program ?? {};
 
  const url = useMemo(
    () =>
      props.key && props.domain
        ? linkConstructor({ key: props.key, domain: props.domain })
        : undefined,
    [props.key, props.domain],
  );
 
  const [dataPersisted, setDataPersisted] = useLocalStorage<QRCodeDesign>(
    `qr-code-design-program-${programEnrollment?.program?.id}`,
    DEFAULT_QR_DESIGN,
  );
 
  const [data, setData] = useState<QRCodeDesign>(dataPersisted);

Sources: apps/web/ui/modals/partner-link-qr-modal.tsx:23-64

Customization Options and Segmented Controls

User interface controls allow fine-grained manipulation of geometric patterns and color properties. Dot styles, marker centers, and marker border configurations use segmented control components to toggle between discrete appearance modes.

Control LabelProperty KeyAvailable ValuesDefault Value
Dot styledotStylesquare, rounded, extra-roundedsquare
Marker centermarkerCenterStylesquare, circlesquare
Marker bordermarkerBorderStylesquare, rounded-square, circlesquare
Foreground ColorfgColorHex color string#000000
Logo VisibilityhideLogotrue, falsefalse (workspace dependent)
export const DEFAULT_QR_DESIGN: QRCodeDesign = {
  fgColor: "#000000",
  hideLogo: false,
  dotStyle: "square",
  markerCenterStyle: "square",
  markerBorderStyle: "square",
};

Sources: apps/web/ui/modals/qr-code-design-fields.tsx:45-51

    <SegmentedControl
      activeIndex={
        data.dotStyle === "rounded"
          ? 1
          : data.dotStyle === "extra-rounded"
            ? 2
            : 0
      }
      count={3}
    >
      <SegmentTab
        active={!data.dotStyle || data.dotStyle === "square"}
        onClick={() => setData((d) => ({ ...d, dotStyle: "square" }))}
        ariaLabel="Square dots"
      >
        <SquareDotIcon />
      </SegmentTab>
      <SegmentTab
        active={data.dotStyle === "rounded"}
        onClick={() => setData((d) => ({ ...d, dotStyle: "rounded" }))}
        ariaLabel="Rounded dots"
      >
        <RoundedDotIcon />
      </SegmentTab>
      <SegmentTab
        active={data.dotStyle === "extra-rounded"}
        onClick={() =>
          setData((d) => ({ ...d, dotStyle: "extra-rounded" }))
        }
        ariaLabel="Extra-rounded dots"
      >
        <ExtraRoundedDotIcon />
      </SegmentTab>
    </SegmentedControl>
  </div>
</div>
 
{/* Marker center + Marker border */}
<div className="grid grid-cols-2 gap-4">
  <div>
    <div className="flex items-center gap-1.5">
      <label className="text-sm font-medium text-neutral-700">
        Marker center
      </label>
    </div>
    <SegmentedControl
      activeIndex={data.markerCenterStyle === "circle" ? 1 : 0}
      count={2}
    >
      <SegmentTab
        active={
          !data.markerCenterStyle || data.markerCenterStyle === "square"
        }
        onClick={() =>
          setData((d) => ({ ...d, markerCenterStyle: "square" }))
        }
        ariaLabel="Square marker center"
      >
        <MarkerCenterSquareIcon />
      </SegmentTab>
      <SegmentTab
        active={data.markerCenterStyle === "circle"}
        onClick={() =>
          setData((d) => ({ ...d, markerCenterStyle: "circle" }))
        }
        ariaLabel="Circle marker center"
      >
        <MarkerCenterCircleIcon />
      </SegmentTab>
    </SegmentedControl>
  </div>
 
  <div>
    <div className="flex items-center gap-1.5">
      <label className="text-sm font-medium text-neutral-700">
        Marker border
      </label>
    </div>
    <SegmentedControl
      activeIndex={

Sources: apps/web/ui/modals/qr-code-design-fields.tsx:165-250

Note

Foreground color changes invoke a debounced callback via useDebouncedCallback with a 500ms delay to prevent excessive re-rendering during color picker adjustments.

const onColorChange = useDebouncedCallback(
  (color: string) => setData((d) => ({ ...d, fgColor: color })),
  500,
);

Sources: apps/web/ui/modals/qr-code-design-fields.tsx:83-86

Live Preview Rendering and Plan Restrictions

The preview container evaluates previewKey strings combining color, logo visibility, and geometric style parameters. When any property changes, AnimatePresence and motion.div apply blur and opacity transitions (initial={{ filter: "blur(2px)", opacity: 0.4 }}) to render live updates of the underlying QRCode component.

const previewKey = `${data.fgColor}-${data.hideLogo}-${data.dotStyle}-${data.markerCenterStyle}-${data.markerBorderStyle}-${data.markerColor ?? ""}`;
 
return (
  <>
    {/* Preview */}
    <div>
      <div className="flex items-center justify-between gap-2">
        <div className="flex items-center gap-2">
          <span className="text-sm font-medium text-neutral-700">
            QR Code Preview
          </span>
          <InfoTooltip content="Customize your QR code to fit your brand. [Learn more.](https://dub.co/help/article/custom-qr-codes)" />
        </div>
        {url && qrData && (
          <div className="flex items-center gap-2">
            <DownloadPopover qrData={qrData} linkProps={linkProps}>
              <div>
                <ButtonTooltip tooltipProps={{ content: "Download QR code" }}>
                  <Download className="h-4 w-4 text-neutral-500" />
                </ButtonTooltip>
              </div>
            </DownloadPopover>
            <CopyPopover qrData={qrData} linkProps={linkProps}>
              <div>
                <ButtonTooltip tooltipProps={{ content: "Copy QR code" }}>
                  <Copy className="h-4 w-4 text-neutral-500" />
                </ButtonTooltip>
              </div>
            </CopyPopover>
          </div>
        )}
      </div>
      <div className="relative mt-2 flex h-44 items-center justify-center overflow-hidden rounded-md border border-neutral-300">
        {!isMobile && (
          <ShimmerDots className="opacity-30 [mask-image:radial-gradient(40%_80%,transparent_50%,black)]" />
        )}
        {url && (
          <AnimatePresence mode="wait">
            <motion.div
              key={previewKey}
              initial={{ filter: "blur(2px)", opacity: 0.4 }}
              animate={{ filter: "blur(0px)", opacity: 1 }}
              exit={{ filter: "blur(2px)", opacity: 0.4 }}
              transition={{ duration: 0.1 }}
              className="relative flex size-full items-center justify-center"
            >
              <QRCode
                url={url}
                fgColor={data.fgColor}
                hideLogo={data.hideLogo}
                logo={logo}
                scale={1}
                dotStyle={data.dotStyle}
                markerCenterStyle={data.markerCenterStyle}
                markerBorderStyle={data.markerBorderStyle}
                markerColor={data.markerColor}
              />
            </motion.div>
          </AnimatePresence>
        )}
      </div>

Sources: apps/web/ui/modals/qr-code-design-fields.tsx:93-153

Warning

Free tier workspaces cannot modify logo visibility settings (hideLogo). The UI disables the logo hide toggle and renders a Crown icon coupled with a TooltipContent component promoting plan upgrades to Pro when non-pro users attempt interaction.

const isPro = plan && plan !== "free";
 
const url = useMemo(
  () =>
    props.key && props.domain
      ? linkConstructor({ key: props.key, domain: props.domain })
      : undefined,
  [props.key, props.domain],
);
 
const [dataPersisted, setDataPersisted] = useLocalStorage<QRCodeDesign>(
  `qr-code-design-${workspaceId}`,
  DEFAULT_QR_DESIGN,
);
 
const [data, setData] = useState<QRCodeDesign>(dataPersisted);
 
const hideLogo = data.hideLogo && !!isPro;
const logo = !isPro
  ? DUB_QR_LOGO
  : domainLogo || workspaceLogo || DUB_QR_LOGO;

Sources: apps/web/ui/modals/link-qr-modal.tsx:69-89

  activeIndex={data.hideLogo && isPro ? 1 : 0}
  count={2}
  disabled={!isPro}
>
  <SegmentTab
    active={!data.hideLogo || !isPro}
    onClick={() => setData((d) => ({ ...d, hideLogo: false }))}
    disabled={!isPro}
  >
    <span className="text-sm">Show</span>
  </SegmentTab>
  <SegmentTab
    active={!!(data.hideLogo && isPro)}
    onClick={() => setData((d) => ({ ...d, hideLogo: true }))}
    disabled={!isPro}
  >
    {!isPro && (
      <Tooltip
        content={
          <TooltipContent
            title="Upgrade to Pro to show or hide your QR code logo."
            cta="Upgrade to Pro"
            href={
              slug ? `/${slug}/upgrade` : "https://dub.co/pricing"
            }
            target="_blank"
          />
        }
      >
        <Crown className="text-content-default mr-1 size-4" />
      </Tooltip>
    )}
    <span className="text-sm">Hide</span>
  </SegmentTab>
</SegmentedControl>

Sources: apps/web/ui/modals/link-qr-modal.tsx:160-194

Was this page helpful?