Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/lib/qr/index.tsx
- apps/web/ui/modals/qr-code-design-fields.tsx
- apps/web/lib/qr/utils.tsx
- apps/web/app/api/qr/route.tsx
- apps/web/lib/qr/api.tsx
- apps/web/ui/placeholders/feature-graphics/qr.tsx
- apps/web/lib/qr/codegen.ts
- apps/web/app/api/og/partner-rewind/route.tsx
- apps/web/app/api/og/avatar/...seed/route.tsx
- apps/web/ui/links/link-builder/qr-code-preview.tsx
- apps/web/ui/shared/qr-code.tsx
- apps/web/lib/openapi/qr/index.ts
- apps/web/ui/shared/icons/qr.tsx
- apps/web/ui/partners/groups/design/previews/embed-preview.tsx
- apps/web/ui/partners/groups/design/previews/portal-preview.tsx
- apps/web/ui/modals/link-qr-modal.tsx
- apps/web/app/ee/app.dub.co/embed/referrals/quickstart.tsx
- apps/web/ui/modals/partner-link-qr-modal.tsx
- apps/web/app/api/og/program/categories/route.tsx
- apps/web/lib/qr/types.ts
- apps/web/lib/zod/schemas/qr.ts
- apps/web/lib/qr/constants.ts
- apps/web/app/ee/partners.dub.co/apply/programSlug/default/apply/success/screenshot.tsx
- packages/ui/src/icons/nucleo/qrcode.tsx
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 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 |
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 Category | Type Name | Supported Values |
|---|---|---|
| Data Modules | DotStyle | "square", "rounded", "extra-rounded" |
| Marker Centers | MarkerCenterStyle | "square", "circle" |
| Marker Borders | MarkerBorderStyle | "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:
| 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. |
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:
- If the target URL does not match a short link (
!shortLink), it returns the default Dub QR logo (DUB_QR_LOGO). - If the associated workspace plan is
"free", it returnsDUB_QR_LOGO. - If
hideLogois set totrue, it returnsnull. - If an explicit
logoparameter is passed, it returns that logo string. - If the link uses a Dub-owned domain (
isDubDomain(shortLink.domain)) and the workspace has no custom logo set, it returnsDUB_QR_LOGO. - For custom domains, it queries domain-level metadata via
getDomainViaEdge(shortLink.domain)and returnsdomain.logo, falling back toworkspace.logo, and ultimately defaulting toDUB_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
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.
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 keyqr-code-design-${workspaceId}with a default configuration defined byDEFAULT_QR_DESIGN. It exposes keyboard shortcut integration viauseLinkBuilderKeyboardShortcutbound to theQkey.PartnerLinkQRModal: Scopes persisted design state to partner programs via local storage keyqr-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 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) |
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
Related
On This Page
OverviewLow-level Matrix GenerationBinary Encoding and SegmentationMatrix Coordinate Computation and MaskingConfiguration and Error LevelsGeometry and Module CustomizationModule and Finder Customization OptionsFinder Pattern Path GenerationLogo Excavation and Error Level AdjustmentClient-Side Rendering and ExportSVG Data URI GenerationCanvas Context RenderingReact Memoization and Scale WrapperServer Engine and Endpoint DeliveryExecution and ValidationRate Limiting and Image ResponseDynamic Logo Resolution via Edge DBInteractive Design ModalsModal Architecture and Layout StructureCustomization Options and Segmented ControlsLive Preview Rendering and Plan RestrictionsRelated