set_custom_domain
Configure a custom domain for QR code short URLs (Pro required). All new QR codes will use your branded domain after setting DNS CNAME. Pass null to remove.
Instructions
Set a custom domain for your QR code short URLs (Pro plan required). When set, all new QR codes will use https://your-domain.com/r/... instead of the default URL. You must configure DNS (CNAME) to point to the QR Agent server. Pass domain=null to remove the custom domain.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| domain | Yes | Your custom domain without protocol (e.g. 'qr.mybrand.com'). Pass null to remove. |
Implementation Reference
- src/modules/auth/auth.service.ts:143-148 (handler)Core database function setCustomDomain that updates the customDomain field on an apiKeys row by keyId using SQLite/Drizzle ORM.
export function setCustomDomain(keyId: number, domain: string | null): void { db.update(apiKeys) .set({ customDomain: domain }) .where(eq(apiKeys.id, keyId)) .run(); } - HTTP API route handler for PUT /api/domain (called by the MCP tool). Validates input, enforces Pro plan gate, checks domain format/uniqueness, calls setCustomDomain(), and checks DNS status.
// PUT /api/domain — set custom domain (Pro only) app.put( "/", { schema: { tags: ["Custom Domain"], summary: "Set your custom domain", description: "Configure a custom domain for your QR code short URLs. Pro plan required. The domain must be unique across all users.", body: { type: "object", required: ["domain"], properties: { domain: { type: "string", description: "Your custom domain without protocol (e.g. 'qr.mybrand.com').", }, }, }, response: { 200: { type: "object", properties: { custom_domain: { type: "string" }, dns_status: { type: "string" }, hint: { type: "string" }, }, }, }, }, }, async (request, reply) => { // Pro-only gate if (request.plan !== "pro") { return sendError(reply, 403, { error: "Custom domains require a Pro plan.", code: "PRO_REQUIRED", hint: "Upgrade to Pro ($19/month) to use custom domains. Use the upgrade_to_pro tool or POST /api/stripe/checkout.", }); } const { domain } = request.body as { domain: string }; // Basic validation: no protocol, no path, no whitespace const cleaned = domain.trim().toLowerCase(); if ( !cleaned || cleaned.includes("://") || cleaned.includes("/") || cleaned.includes(" ") ) { return sendError(reply, 400, { error: "Invalid domain format.", code: "INVALID_DOMAIN", hint: "Provide a bare domain without protocol or path (e.g. 'qr.mybrand.com', not 'https://qr.mybrand.com/').", }); } // Must contain at least one dot if (!cleaned.includes(".")) { return sendError(reply, 400, { error: "Invalid domain format.", code: "INVALID_DOMAIN", hint: "Provide a fully qualified domain name with at least one dot (e.g. 'qr.mybrand.com').", }); } // Uniqueness check if (isCustomDomainTaken(cleaned, request.apiKeyId)) { return sendError(reply, 409, { error: `Domain "${cleaned}" is already claimed by another user.`, code: "DOMAIN_ALREADY_TAKEN", hint: "Choose a different subdomain or contact support if you believe this is an error.", }); } setCustomDomain(request.apiKeyId, cleaned); const dnsStatus = await checkDnsStatus(cleaned); return { custom_domain: cleaned, dns_status: dnsStatus, hint: dnsStatus === "active" ? `Domain ${cleaned} is active. New QR codes will use https://${cleaned}/r/...` : `Domain ${cleaned} saved. DNS is pending — add a CNAME record pointing to your server. Use GET /api/domain to re-check status.`, }; } );