---
title: "QR Code Generator"
description: "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 TypeS..."
last_updated: "2026-10-12T07:00:10.151461+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-attribution/qr-code-generator"
---

<details>
<summary>Relevant source files</summary>

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

- [apps/web/lib/qr/index.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx)
- [apps/web/ui/modals/qr-code-design-fields.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx)
- [apps/web/lib/qr/utils.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/utils.tsx)
- [apps/web/app/api/qr/route.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx)
- [apps/web/lib/qr/api.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/api.tsx)
- [apps/web/ui/placeholders/feature-graphics/qr.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/placeholders/feature-graphics/qr.tsx)
- [apps/web/lib/qr/codegen.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/codegen.ts)
- [apps/web/app/api/og/partner-rewind/route.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/og/partner-rewind/route.tsx)
- [apps/web/app/api/og/avatar/...seed/route.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/og/avatar/%5B%5B...seed%5D%5D/route.tsx)
- [apps/web/ui/links/link-builder/qr-code-preview.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/links/link-builder/qr-code-preview.tsx)
- [apps/web/ui/shared/qr-code.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/shared/qr-code.tsx)
- [apps/web/lib/openapi/qr/index.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/qr/index.ts)
- [apps/web/ui/shared/icons/qr.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/shared/icons/qr.tsx)
- [apps/web/ui/partners/groups/design/previews/embed-preview.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/partners/groups/design/previews/embed-preview.tsx)
- [apps/web/ui/partners/groups/design/previews/portal-preview.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/partners/groups/design/previews/portal-preview.tsx)
- [apps/web/ui/modals/link-qr-modal.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/link-qr-modal.tsx)
- [apps/web/app/ee/app.dub.co/embed/referrals/quickstart.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx)
- [apps/web/ui/modals/partner-link-qr-modal.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/partner-link-qr-modal.tsx)
- [apps/web/app/api/og/program/categories/route.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/og/program/categories/route.tsx)
- [apps/web/lib/qr/types.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/types.ts)
- [apps/web/lib/zod/schemas/qr.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/zod/schemas/qr.ts)
- [apps/web/lib/qr/constants.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/constants.ts)
- [apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/success/screenshot.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/partners.dub.co/(apply)/%5BprogramSlug%5D/(default)/apply/success/screenshot.tsx)
- [packages/ui/src/icons/nucleo/qrcode.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/packages/ui/src/icons/nucleo/qrcode.tsx)
</details>

## 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/codegen.ts#L1-L32), [apps/web/app/api/qr/route.tsx:18-58](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L18-L58)

## 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.

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/codegen.ts#L68-L151)

> [!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.

```typescript
// 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/codegen.ts#L155-L219)

### 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 Key | Internal Ecc Constant | Error Correction Capability |
| :--- | :--- | :--- |
| `L` | `qrcodegen.QrCode.Ecc.LOW` | Recovers ~7% of data |
| `M` | `qrcodegen.QrCode.Ecc.MEDIUM` | Recovers ~15% of data |
| `Q` | `qrcodegen.QrCode.Ecc.QUARTILE` | Recovers ~25% of data |
| `H` | `qrcodegen.QrCode.Ecc.HIGH` | Recovers ~30% of data |

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/constants.ts#L3-L8)

## 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 Category | Type Name | Supported Values |
| :--- | :--- | :--- |
| Data Modules | `DotStyle` | `"square"`, `"rounded"`, `"extra-rounded"` |
| Marker Centers | `MarkerCenterStyle` | `"square"`, `"circle"` |
| Marker Borders | `MarkerBorderStyle` | `"square"`, `"rounded-square"`, `"circle"` |

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/types.ts#L7-L9)

### 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/utils.tsx#L378-L403)

> [!NOTE]
> When `markerColor` is omitted from the QR configuration, the generator falls back to using the primary foreground color (`fgColor`).

```tsx
const effectiveMarkerColor = markerColor ?? fgColor;
```
Sources: [apps/web/lib/qr/utils.tsx:471](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/utils.tsx#L471)

### 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/utils.tsx#L307-L314)

> [!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.

```tsx
const shouldUseHigherErrorLevel =
  isOGContext && imageSettings?.excavate && (level === "L" || level === "M");

const effectiveLevel = shouldUseHigherErrorLevel ? "Q" : level;
```
Sources: [apps/web/lib/qr/utils.tsx:473-476](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/utils.tsx#L473-L476)

## 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"`.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L315-L360)

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.

```tsx
  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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L362-L399)

> [!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")`.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L401-L424)

### 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`.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L440-L494)

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.

```tsx
// 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L495-L540)

> [!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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/qr/index.tsx#L426-L438)

### 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/shared/qr-code.tsx#L6-L84)

## 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L11-L21)

```typescript
export const qrCodePaths: ZodOpenApiPathsObject = {
  "/qr": {
    get: getQRCode,
  },
};
```
Sources: [apps/web/lib/openapi/qr/index.ts:29-33](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/qr/index.ts#L29-L33)

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

| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `url` | string (parsed URL) | — | The URL to generate a QR code for. |
| `logo` | string (optional) | undefined | The logo to include in the QR code (restricted to paid plans). |
| `size` | number (coerced) | `600` | The size of the QR code in pixels. |
| `level` | enum (`QR_LEVELS`) | `"L"` | The error correction level (`L`, `M`, `Q`, `H`). |
| `fgColor` | string (optional) | `DEFAULT_FGCOLOR` (`#000000`) | The foreground color of the QR code in hex format. |
| `bgColor` | string (optional) | `DEFAULT_BGCOLOR` (`#ffffff`) | The background color of the QR code in hex format. |
| `hideLogo` | boolean query | `false` | Whether to hide the logo in the QR code (restricted to paid plans). |
| `margin` | number (coerced) | `DEFAULT_MARGIN` | The size of the margin around the QR code. |
| `includeMargin` | boolean query | `true` | Deprecated margin inclusion toggle. |

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/zod/schemas/qr.ts#L11-L67)

> [!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.

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/zod/schemas/qr.ts#L60-L66)

### 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`).

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L22-L54)

```typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/openapi/qr/index.ts#L7-L27)

> [!NOTE]
> Preflight `OPTIONS` requests are handled explicitly by returning an empty `Response` with status `204` and the shared `CORS_HEADERS`.

```tsx
export function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: CORS_HEADERS,
  });
}
```
Sources: [apps/web/app/api/qr/route.tsx:106-111](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L106-L111)

### 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L60-L69)

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`.

```tsx

  // 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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/qr/route.tsx#L70-L104)

## Interactive Design Modals

### Modal Architecture and Layout Structure

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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx#L60-L79)

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](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/partner-programs/partner-programs) via local storage key `qr-code-design-program-${programEnrollment?.program?.id}`.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/link-qr-modal.tsx#L37-L85)

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/partner-link-qr-modal.tsx#L23-L64)

### 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 Label | Property Key | Available Values | Default Value |
| :--- | :--- | :--- | :--- |
| **Dot style** | `dotStyle` | `square`, `rounded`, `extra-rounded` | `square` |
| **Marker center** | `markerCenterStyle` | `square`, `circle` | `square` |
| **Marker border** | `markerBorderStyle` | `square`, `rounded-square`, `circle` | `square` |
| **Foreground Color** | `fgColor` | Hex color string | `#000000` |
| **Logo Visibility** | `hideLogo` | `true`, `false` | `false` (workspace dependent) |

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx#L45-L51)

```tsx
    <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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx#L165-L250)

> [!NOTE]
> Foreground color changes invoke a debounced callback via `useDebouncedCallback` with a 500ms delay to prevent excessive re-rendering during color picker adjustments.

```tsx
const onColorChange = useDebouncedCallback(
  (color: string) => setData((d) => ({ ...d, fgColor: color })),
  500,
);
```
Sources: [apps/web/ui/modals/qr-code-design-fields.tsx:83-86](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx#L83-L86)

### 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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/qr-code-design-fields.tsx#L93-L153)

> [!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.

```tsx
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/link-qr-modal.tsx#L69-L89)

```tsx
  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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/ui/modals/link-qr-modal.tsx#L160-L194)

## Related

- [Link Dispatch](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-attribution/link-dispatch)
- [REST API](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/administration-and-api/rest-api)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
