From db3298e9fe91432b5a2a094ef83cd89b5d873775 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 12 Feb 2026 17:14:27 +0300 Subject: [PATCH 1/4] refactor: move custom domains to tanstack db --- .../[projectId]/(overview)/data-provider.tsx | 21 +++- .../add-custom-domain.tsx | 51 ++++---- .../custom-domain-row.tsx | 26 ++-- .../hooks/use-custom-domains-manager.ts | 40 ------- .../details/custom-domains-section/index.tsx | 21 ++-- .../details/custom-domains-section/types.ts | 20 +--- .../lib/collections/deploy/custom-domains.ts | 113 ++++++++++++++++++ web/apps/dashboard/lib/collections/index.ts | 3 + 8 files changed, 181 insertions(+), 114 deletions(-) delete mode 100644 web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/hooks/use-custom-domains-manager.ts create mode 100644 web/apps/dashboard/lib/collections/deploy/custom-domains.ts diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx index cfbd8a4d06..1ff58da091 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx @@ -1,6 +1,7 @@ "use client"; import { collection } from "@/lib/collections"; +import type { CustomDomain } from "@/lib/collections/deploy/custom-domains"; import type { Deployment } from "@/lib/collections/deploy/deployments"; import type { Domain } from "@/lib/collections/deploy/domains"; import type { Environment } from "@/lib/collections/deploy/environments"; @@ -18,10 +19,12 @@ type ProjectDataContextType = { domains: Domain[]; deployments: Deployment[]; environments: Environment[]; + customDomains: CustomDomain[]; isDomainsLoading: boolean; isDeploymentsLoading: boolean; isEnvironmentsLoading: boolean; + isCustomDomainsLoading: boolean; getDomainsForDeployment: (deploymentId: string) => Domain[]; getLiveDomains: () => Domain[]; @@ -30,6 +33,7 @@ type ProjectDataContextType = { refetchDomains: () => void; refetchDeployments: () => void; + refetchCustomDomains: () => void; refetchAll: () => void; }; @@ -73,10 +77,20 @@ export const ProjectDataProvider = ({ children }: PropsWithChildren) => { [projectId], ); + const customDomainsQuery = useLiveQuery( + (q) => + q + .from({ customDomain: collection.customDomains }) + .where(({ customDomain }) => eq(customDomain.projectId, projectId)) + .orderBy(({ customDomain }) => customDomain.createdAt, "desc"), + [projectId], + ); + const value = useMemo(() => { const domains = domainsQuery.data ?? []; const deployments = deploymentsQuery.data ?? []; const environments = environmentsQuery.data ?? []; + const customDomains = customDomainsQuery.data ?? []; const project = projectQuery.data?.at(0); return { @@ -94,6 +108,9 @@ export const ProjectDataProvider = ({ children }: PropsWithChildren) => { environments, isEnvironmentsLoading: environmentsQuery.isLoading, + customDomains, + isCustomDomainsLoading: customDomainsQuery.isLoading, + getDomainsForDeployment: (deploymentId: string) => domains.filter((d) => d.deploymentId === deploymentId), @@ -106,14 +123,16 @@ export const ProjectDataProvider = ({ children }: PropsWithChildren) => { refetchDomains: () => collection.domains.utils.refetch(), refetchDeployments: () => collection.deployments.utils.refetch(), + refetchCustomDomains: () => collection.customDomains.utils.refetch(), refetchAll: () => { collection.projects.utils.refetch(); collection.deployments.utils.refetch(); collection.domains.utils.refetch(); collection.environments.utils.refetch(); + collection.customDomains.utils.refetch(); }, }; - }, [projectId, domainsQuery, deploymentsQuery, projectQuery, environmentsQuery]); + }, [projectId, domainsQuery, deploymentsQuery, projectQuery, environmentsQuery, customDomainsQuery]); return {children}; }; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx index 6f3b8f32e1..3aa19cf514 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx @@ -1,5 +1,5 @@ "use client"; -import { trpc } from "@/lib/trpc/client"; +import { collection } from "@/lib/collections"; import { cn } from "@/lib/utils"; import { Button, @@ -9,7 +9,6 @@ import { SelectItem, SelectTrigger, SelectValue, - toast, } from "@unkey/ui"; import { useEffect, useRef, useState } from "react"; import { useProjectData } from "../../data-provider"; @@ -43,11 +42,11 @@ export function AddCustomDomain({ onSuccess, }: AddCustomDomainProps) { const { projectId } = useProjectData(); - const addMutation = trpc.deploy.customDomain.add.useMutation(); const containerRef = useRef(null); const inputRef = useRef(null); const [domain, setDomain] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); // Default to production environment, fall back to first environment const defaultEnvId = environments.find((e) => e.slug === "production")?.id ?? environments[0]?.id ?? ""; @@ -61,8 +60,6 @@ export function AddCustomDomain({ inputRef.current?.focus(); }, []); - const isSubmitting = addMutation.isLoading; - const getError = (): string | undefined => { if (!domain) { return undefined; @@ -96,28 +93,32 @@ export function AddCustomDomain({ return; } - const mutation = addMutation.mutateAsync({ - projectId, - environmentId, - domain, - }); - - toast.promise(mutation, { - loading: "Adding domain...", - success: (data) => ({ - message: "Domain added", - description: `Add a CNAME record pointing to ${data.targetCname}`, - }), - error: (err) => ({ - message: "Failed to add domain", - description: err.message, - }), - }); - + setIsSubmitting(true); try { - await mutation; + const tx = collection.customDomains.insert({ + id: crypto.randomUUID(), + domain, + workspaceId: "", + projectId, + environmentId, + verificationStatus: "pending", + verificationToken: "", + ownershipVerified: false, + cnameVerified: false, + targetCname: "", + checkAttempts: 0, + lastCheckedAt: null, + verificationError: null, + createdAt: Date.now(), + updatedAt: null, + }); + await tx.isPersisted.promise; onSuccess(); - } catch {} + } catch { + // Toast handled by collection onInsert handler + } finally { + setIsSubmitting(false); + } }; return ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx index 7eb518a1be..cfe2441347 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx @@ -1,4 +1,5 @@ "use client"; +import { collection } from "@/lib/collections"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { @@ -59,32 +60,21 @@ const statusConfig: Record< export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowProps) { const { projectId } = useProjectData(); - const deleteMutation = trpc.deploy.customDomain.delete.useMutation(); const retryMutation = trpc.deploy.customDomain.retry.useMutation(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const deleteButtonRef = useRef(null); const status = statusConfig[domain.verificationStatus]; const handleDelete = async () => { - const mutation = deleteMutation.mutateAsync({ - domain: domain.domain, - projectId, - }); - - toast.promise(mutation, { - loading: "Deleting domain...", - success: "Domain deleted", - error: (err) => ({ - message: "Failed to delete domain", - description: err.message, - }), - }); - + setIsDeleting(true); try { - await mutation; + collection.customDomains.delete(domain.id); onDelete(); - } catch {} + } finally { + setIsDeleting(false); + } }; const handleRetry = async () => { @@ -108,7 +98,7 @@ export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowPr } catch {} }; - const isLoading = deleteMutation.isLoading || retryMutation.isLoading; + const isLoading = isDeleting || retryMutation.isLoading; return (
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/hooks/use-custom-domains-manager.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/hooks/use-custom-domains-manager.ts deleted file mode 100644 index 6f1682c873..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/hooks/use-custom-domains-manager.ts +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; -import { trpc } from "@/lib/trpc/client"; - -type UseCustomDomainsManagerProps = { - projectId: string; -}; - -export function useCustomDomainsManager({ projectId }: UseCustomDomainsManagerProps) { - const { data, isLoading, error } = trpc.deploy.customDomain.list.useQuery( - { projectId }, - { - refetchInterval: (queryData) => { - const hasPending = queryData?.some( - (d) => d.verificationStatus === "pending" || d.verificationStatus === "verifying", - ); - return hasPending ? 5_000 : false; - }, - }, - ); - - const utils = trpc.useUtils(); - - const invalidate = () => { - utils.deploy.customDomain.list.invalidate({ projectId }); - }; - - const customDomains = data ?? []; - - const getExistingDomain = (domain: string) => { - return customDomains.find((d) => d.domain.toLowerCase() === domain.toLowerCase()); - }; - - return { - customDomains, - isLoading, - error, - getExistingDomain, - invalidate, - }; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx index 254fdbb9c7..3b75078d5d 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx @@ -7,19 +7,18 @@ import { EmptySection } from "../../components/empty-section"; import { useProjectData } from "../../data-provider"; import { AddCustomDomain } from "./add-custom-domain"; import { CustomDomainRow, CustomDomainRowSkeleton } from "./custom-domain-row"; -import { useCustomDomainsManager } from "./hooks/use-custom-domains-manager"; type CustomDomainsSectionProps = { environments: Array<{ id: string; slug: string }>; }; export function CustomDomainsSection({ environments }: CustomDomainsSectionProps) { - const { projectId } = useProjectData(); - const { customDomains, isLoading, getExistingDomain, invalidate } = useCustomDomainsManager({ - projectId, - }); + const { customDomains, isCustomDomainsLoading, refetchCustomDomains } = useProjectData(); const [isAddingNew, setIsAddingNew] = useState(false); + const getExistingDomain = (domain: string) => + customDomains.find((d) => d.domain.toLowerCase() === domain.toLowerCase()); + const startAdding = () => setIsAddingNew(true); const cancelAdding = () => setIsAddingNew(false); @@ -27,12 +26,12 @@ export function CustomDomainsSection({ environments }: CustomDomainsSectionProps
{/* Domain list */}
- {isLoading ? ( + {isCustomDomainsLoading ? ( <> @@ -42,8 +41,8 @@ export function CustomDomainsSection({ environments }: CustomDomainsSectionProps )) )} @@ -54,13 +53,13 @@ export function CustomDomainsSection({ environments }: CustomDomainsSectionProps getExistingDomain={getExistingDomain} onCancel={cancelAdding} onSuccess={() => { - invalidate(); + refetchCustomDomains(); cancelAdding(); }} /> )} - {customDomains.length === 0 && !isAddingNew && !isLoading && ( + {customDomains.length === 0 && !isAddingNew && !isCustomDomainsLoading && ( 0} /> )}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/types.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/types.ts index c669230e3b..21125e27bc 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/types.ts +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/types.ts @@ -1,19 +1 @@ -export type VerificationStatus = "pending" | "verifying" | "verified" | "failed"; - -export type CustomDomain = { - id: string; - domain: string; - workspaceId: string; - projectId: string; - environmentId: string; - verificationStatus: VerificationStatus; - verificationToken: string; - ownershipVerified: boolean; - cnameVerified: boolean; - targetCname: string; - checkAttempts: number; - lastCheckedAt: number | null; - verificationError: string | null; - createdAt: number; - updatedAt: number | null; -}; +export type { CustomDomain, VerificationStatus } from "@/lib/collections/deploy/custom-domains"; diff --git a/web/apps/dashboard/lib/collections/deploy/custom-domains.ts b/web/apps/dashboard/lib/collections/deploy/custom-domains.ts new file mode 100644 index 0000000000..788d320c91 --- /dev/null +++ b/web/apps/dashboard/lib/collections/deploy/custom-domains.ts @@ -0,0 +1,113 @@ +"use client"; +import { queryCollectionOptions } from "@tanstack/query-db-collection"; +import { createCollection } from "@tanstack/react-db"; +import { toast } from "@unkey/ui"; +import { z } from "zod"; +import { queryClient, trpcClient } from "../client"; +import { parseProjectIdFromWhere, validateProjectIdInQuery } from "./utils"; + +const verificationStatusSchema = z.enum(["pending", "verifying", "verified", "failed"]); + +const schema = z.object({ + id: z.string(), + domain: z.string(), + workspaceId: z.string(), + projectId: z.string(), + environmentId: z.string(), + verificationStatus: verificationStatusSchema, + verificationToken: z.string(), + ownershipVerified: z.boolean(), + cnameVerified: z.boolean(), + targetCname: z.string(), + checkAttempts: z.number(), + lastCheckedAt: z.number().nullable(), + verificationError: z.string().nullable(), + createdAt: z.number(), + updatedAt: z.number().nullable(), +}); + +export type CustomDomain = z.infer; +export type VerificationStatus = z.infer; + +/** + * Custom domains collection. + * + * IMPORTANT: All queries MUST filter by projectId: + * .where(({ customDomain }) => eq(customDomain.projectId, projectId)) + */ +export const customDomains = createCollection( + queryCollectionOptions({ + queryClient, + syncMode: "on-demand", + refetchInterval: 5000, + queryKey: (opts) => { + const projectId = parseProjectIdFromWhere(opts.where); + return projectId ? ["customDomains", projectId] : ["customDomains"]; + }, + retry: 3, + queryFn: async (ctx) => { + const options = ctx.meta?.loadSubsetOptions; + + validateProjectIdInQuery(options?.where); + const projectId = parseProjectIdFromWhere(options?.where); + + if (!projectId) { + throw new Error("Query must include eq(collection.projectId, projectId) constraint"); + } + + return trpcClient.deploy.customDomain.list.query({ projectId }); + }, + getKey: (item) => item.id, + id: "customDomains", + onInsert: async ({ transaction }) => { + const { changes } = transaction.mutations[0]; + + const addInput = z + .object({ + projectId: z.string().min(1), + environmentId: z.string().min(1), + domain: z.string().min(1), + }) + .parse({ + projectId: changes.projectId, + environmentId: changes.environmentId, + domain: changes.domain, + }); + + const mutation = trpcClient.deploy.customDomain.add.mutate(addInput); + + toast.promise(mutation, { + loading: "Adding domain...", + success: (data) => ({ + message: "Domain added", + description: `Add a CNAME record pointing to ${data.targetCname}`, + }), + error: (err) => ({ + message: "Failed to add domain", + description: err.message, + }), + }); + + await mutation; + }, + onDelete: async ({ transaction }) => { + const original = transaction.mutations[0].original; + + const deleteMutation = trpcClient.deploy.customDomain.delete.mutate({ + domain: original.domain, + projectId: original.projectId, + }); + + toast.promise(deleteMutation, { + loading: "Deleting domain...", + success: "Domain deleted", + error: (err) => ({ + message: "Failed to delete domain", + description: err.message, + }), + }); + + await deleteMutation; + }, + }), +); diff --git a/web/apps/dashboard/lib/collections/index.ts b/web/apps/dashboard/lib/collections/index.ts index 7cff5300a2..b7e98dcae0 100644 --- a/web/apps/dashboard/lib/collections/index.ts +++ b/web/apps/dashboard/lib/collections/index.ts @@ -1,4 +1,5 @@ "use client"; +import { customDomains } from "./deploy/custom-domains"; import { deployments } from "./deploy/deployments"; import { domains } from "./deploy/domains"; import { environments } from "./deploy/environments"; @@ -7,6 +8,7 @@ import { ratelimitNamespaces } from "./ratelimit/namespaces"; import { ratelimitOverrides } from "./ratelimit/overrides"; // Export types +export type { CustomDomain } from "./deploy/custom-domains"; export type { Deployment } from "./deploy/deployments"; export type { Domain } from "./deploy/domains"; export type { Project } from "./deploy/projects"; @@ -22,6 +24,7 @@ export const collection = { environments, domains, deployments, + customDomains, } as const; export async function reset() { From d16dca5952d0a5ee4cb9ca402dd2dc817636f986 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 12 Feb 2026 17:38:42 +0300 Subject: [PATCH 2/4] fix: comment --- .../[projectId]/(overview)/data-provider.tsx | 9 ++++++++- .../add-custom-domain.tsx | 18 ++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx index 1ff58da091..36c37733b4 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/data-provider.tsx @@ -132,7 +132,14 @@ export const ProjectDataProvider = ({ children }: PropsWithChildren) => { collection.customDomains.utils.refetch(); }, }; - }, [projectId, domainsQuery, deploymentsQuery, projectQuery, environmentsQuery, customDomainsQuery]); + }, [ + projectId, + domainsQuery, + deploymentsQuery, + projectQuery, + environmentsQuery, + customDomainsQuery, + ]); return {children}; }; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx index 3aa19cf514..7b6cedeb94 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx @@ -46,7 +46,6 @@ export function AddCustomDomain({ const inputRef = useRef(null); const [domain, setDomain] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); // Default to production environment, fall back to first environment const defaultEnvId = environments.find((e) => e.slug === "production")?.id ?? environments[0]?.id ?? ""; @@ -80,7 +79,7 @@ export function AddCustomDomain({ const isValid = domain && !error && environmentId; const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && isValid && !isSubmitting) { + if (e.key === "Enter" && isValid) { e.preventDefault(); handleSave(); } else if (e.key === "Escape") { @@ -89,11 +88,10 @@ export function AddCustomDomain({ }; const handleSave = async () => { - if (!isValid || isSubmitting) { + if (!isValid) { return; } - setIsSubmitting(true); try { const tx = collection.customDomains.insert({ id: crypto.randomUUID(), @@ -116,8 +114,6 @@ export function AddCustomDomain({ onSuccess(); } catch { // Toast handled by collection onInsert handler - } finally { - setIsSubmitting(false); } }; @@ -155,17 +151,11 @@ export function AddCustomDomain({ variant="primary" onClick={handleSave} className="h-8 text-xs px-3" - disabled={!isValid || isSubmitting} - loading={isSubmitting} + disabled={!isValid} > Add -
From e64897e562d9334265e728bb7af3c07cba0c4b60 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 12 Feb 2026 18:04:53 +0300 Subject: [PATCH 3/4] fix: delete mutation --- .../add-custom-domain.tsx | 53 ++++---- .../custom-domain-row.tsx | 114 +++--------------- .../details/custom-domains-section/index.tsx | 17 +-- .../lib/collections/deploy/custom-domains.ts | 19 +++ 4 files changed, 62 insertions(+), 141 deletions(-) diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx index 7b6cedeb94..e4aa2ba281 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/add-custom-domain.tsx @@ -31,15 +31,13 @@ function extractDomain(input: string): string { type AddCustomDomainProps = { environments: Array<{ id: string; slug: string }>; getExistingDomain: (domain: string) => CustomDomain | undefined; - onCancel: () => void; - onSuccess: () => void; + onDismiss: () => void; }; export function AddCustomDomain({ environments, getExistingDomain, - onCancel, - onSuccess, + onDismiss, }: AddCustomDomainProps) { const { projectId } = useProjectData(); const containerRef = useRef(null); @@ -83,38 +81,33 @@ export function AddCustomDomain({ e.preventDefault(); handleSave(); } else if (e.key === "Escape") { - onCancel(); + onDismiss(); } }; - const handleSave = async () => { + const handleSave = () => { if (!isValid) { return; } - try { - const tx = collection.customDomains.insert({ - id: crypto.randomUUID(), - domain, - workspaceId: "", - projectId, - environmentId, - verificationStatus: "pending", - verificationToken: "", - ownershipVerified: false, - cnameVerified: false, - targetCname: "", - checkAttempts: 0, - lastCheckedAt: null, - verificationError: null, - createdAt: Date.now(), - updatedAt: null, - }); - await tx.isPersisted.promise; - onSuccess(); - } catch { - // Toast handled by collection onInsert handler - } + collection.customDomains.insert({ + id: crypto.randomUUID(), + domain, + workspaceId: "", + projectId, + environmentId, + verificationStatus: "pending", + verificationToken: "", + ownershipVerified: false, + cnameVerified: false, + targetCname: "", + checkAttempts: 0, + lastCheckedAt: null, + verificationError: null, + createdAt: Date.now(), + updatedAt: null, + }); + onDismiss(); }; return ( @@ -155,7 +148,7 @@ export function AddCustomDomain({ > Add -
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx index cfe2441347..22d3349f98 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/custom-domain-row.tsx @@ -1,6 +1,6 @@ "use client"; import { collection } from "@/lib/collections"; -import { trpc } from "@/lib/trpc/client"; +import { retryDomainVerification } from "@/lib/collections/deploy/custom-domains"; import { cn } from "@/lib/utils"; import { CircleCheck, @@ -20,16 +20,13 @@ import { Tooltip, TooltipContent, TooltipTrigger, - toast, } from "@unkey/ui"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { useProjectData } from "../../data-provider"; import type { CustomDomain, VerificationStatus } from "./types"; type CustomDomainRowProps = { domain: CustomDomain; - onDelete: () => void; - onRetry: () => void; }; const statusConfig: Record< @@ -58,48 +55,27 @@ const statusConfig: Record< }, }; -export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowProps) { +export function CustomDomainRow({ domain }: CustomDomainRowProps) { const { projectId } = useProjectData(); - const retryMutation = trpc.deploy.customDomain.retry.useMutation(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); + const [isRetrying, setIsRetrying] = useState(false); const deleteButtonRef = useRef(null); const status = statusConfig[domain.verificationStatus]; - const handleDelete = async () => { - setIsDeleting(true); - try { - collection.customDomains.delete(domain.id); - onDelete(); - } finally { - setIsDeleting(false); - } + const handleDelete = () => { + collection.customDomains.delete(domain.id); }; const handleRetry = async () => { - const mutation = retryMutation.mutateAsync({ - domain: domain.domain, - projectId, - }); - - toast.promise(mutation, { - loading: "Retrying verification...", - success: "Verification restarted", - error: (err) => ({ - message: "Failed to retry verification", - description: err.message, - }), - }); - + setIsRetrying(true); try { - await mutation; - onRetry(); - } catch {} + await retryDomainVerification({ domain: domain.domain, projectId }); + } finally { + setIsRetrying(false); + } }; - const isLoading = isDeleting || retryMutation.isLoading; - return (
@@ -128,12 +104,10 @@ export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowPr size="icon" variant="outline" onClick={handleRetry} - disabled={isLoading} + disabled={isRetrying} className="size-7 text-gray-9 hover:text-gray-11" > - + Retry verification @@ -153,9 +127,8 @@ export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowPr ref={deleteButtonRef} size="icon" variant="outline" - disabled={isLoading} onClick={() => setIsConfirmOpen(true)} - className="size-7 text-gray-9 hover:text-error-9 opacity-0 group-hover:opacity-100 transition-opacity" + className="size-7 text-gray-9 hover:text-error-9" > @@ -182,7 +155,6 @@ export function CustomDomainRow({ domain, onDelete, onRetry }: CustomDomainRowPr verificationToken={domain.verificationToken} ownershipVerified={domain.ownershipVerified} cnameVerified={domain.cnameVerified} - projectId={projectId} /> )}
@@ -195,59 +167,15 @@ type DnsRecordTableProps = { verificationToken: string; ownershipVerified: boolean; cnameVerified: boolean; - projectId: string; }; -// Backend checks every 60 seconds via Restate -const CHECK_INTERVAL_MS = 60 * 1000; - function DnsRecordTable({ domain, targetCname, - verificationToken: initialVerificationToken, - ownershipVerified: initialOwnershipVerified, - cnameVerified: initialCnameVerified, - projectId, + verificationToken, + ownershipVerified, + cnameVerified, }: DnsRecordTableProps) { - const [secondsUntilCheck, setSecondsUntilCheck] = useState(CHECK_INTERVAL_MS / 1000); - - // Poll for DNS status updates - only fetches this specific domain - const { - data: dnsStatus, - dataUpdatedAt, - isFetching, - } = trpc.deploy.customDomain.checkDns.useQuery( - { domain, projectId }, - { - refetchInterval: CHECK_INTERVAL_MS, - refetchIntervalInBackground: false, - }, - ); - - // Use live data if available, otherwise fall back to initial props - const verificationToken = dnsStatus?.verificationToken ?? initialVerificationToken; - const ownershipVerified = dnsStatus?.ownershipVerified ?? initialOwnershipVerified; - const cnameVerified = dnsStatus?.cnameVerified ?? initialCnameVerified; - - useEffect(() => { - const calculateSecondsRemaining = () => { - if (!dataUpdatedAt) { - return CHECK_INTERVAL_MS / 1000; - } - const nextCheckAt = dataUpdatedAt + CHECK_INTERVAL_MS; - const remaining = Math.max(0, Math.ceil((nextCheckAt - Date.now()) / 1000)); - return remaining; - }; - - setSecondsUntilCheck(calculateSecondsRemaining()); - - const interval = setInterval(() => { - setSecondsUntilCheck(calculateSecondsRemaining()); - }, 1000); - - return () => clearInterval(interval); - }, [dataUpdatedAt]); - const txtRecordName = `_unkey.${domain}`; const txtRecordValue = `unkey-domain-verify=${verificationToken}`; @@ -328,14 +256,6 @@ function DnsRecordTable({
- - {/* Next check countdown */} -
- - - {isFetching ? "Refreshing..." : `Next check in ${secondsUntilCheck}s`} - -
); } diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx index 3b75078d5d..f8212f0efd 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/custom-domains-section/index.tsx @@ -13,7 +13,7 @@ type CustomDomainsSectionProps = { }; export function CustomDomainsSection({ environments }: CustomDomainsSectionProps) { - const { customDomains, isCustomDomainsLoading, refetchCustomDomains } = useProjectData(); + const { customDomains, isCustomDomainsLoading } = useProjectData(); const [isAddingNew, setIsAddingNew] = useState(false); const getExistingDomain = (domain: string) => @@ -37,25 +37,14 @@ export function CustomDomainsSection({ environments }: CustomDomainsSectionProps ) : ( - customDomains.map((domain) => ( - - )) + customDomains.map((domain) => ) )} {isAddingNew && ( { - refetchCustomDomains(); - cancelAdding(); - }} + onDismiss={cancelAdding} /> )} diff --git a/web/apps/dashboard/lib/collections/deploy/custom-domains.ts b/web/apps/dashboard/lib/collections/deploy/custom-domains.ts index 788d320c91..01211fb482 100644 --- a/web/apps/dashboard/lib/collections/deploy/custom-domains.ts +++ b/web/apps/dashboard/lib/collections/deploy/custom-domains.ts @@ -111,3 +111,22 @@ export const customDomains = createCollection( }, }), ); + +export async function retryDomainVerification({ + domain, + projectId, +}: { domain: string; projectId: string }): Promise { + const mutation = trpcClient.deploy.customDomain.retry.mutate({ domain, projectId }); + + toast.promise(mutation, { + loading: "Retrying verification...", + success: "Verification restarted", + error: (err) => ({ + message: "Failed to retry verification", + description: err.message, + }), + }); + + await mutation; + await customDomains.utils.refetch(); +} From 20aca56616d2983666d0beef14382f97f8775d4b Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 12 Feb 2026 18:17:06 +0300 Subject: [PATCH 4/4] remove: unnecessary query --- .../deploy/custom-domains/check-dns.ts | 63 ------------------- web/apps/dashboard/lib/trpc/routers/index.ts | 2 - 2 files changed, 65 deletions(-) delete mode 100644 web/apps/dashboard/lib/trpc/routers/deploy/custom-domains/check-dns.ts diff --git a/web/apps/dashboard/lib/trpc/routers/deploy/custom-domains/check-dns.ts b/web/apps/dashboard/lib/trpc/routers/deploy/custom-domains/check-dns.ts deleted file mode 100644 index 781bfda41e..0000000000 --- a/web/apps/dashboard/lib/trpc/routers/deploy/custom-domains/check-dns.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db } from "@/lib/db"; -import { ratelimit, withRatelimit, workspaceProcedure } from "@/lib/trpc/trpc"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; - -export const checkDns = workspaceProcedure - .use(withRatelimit(ratelimit.read)) - .input( - z.object({ - domain: z.string().min(1, "Domain is required"), - projectId: z.string().min(1, "Project ID is required"), - }), - ) - .query(async ({ input, ctx }) => { - // Verify project belongs to workspace - const project = await db.query.projects.findFirst({ - where: (table, { eq, and }) => - and(eq(table.id, input.projectId), eq(table.workspaceId, ctx.workspace.id)), - columns: { - id: true, - }, - }); - - if (!project) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Project not found", - }); - } - - // Get the domain record - const domainRecord = await db.query.customDomains.findFirst({ - where: (table, { eq, and }) => - and(eq(table.domain, input.domain), eq(table.projectId, input.projectId)), - columns: { - id: true, - domain: true, - verificationToken: true, - ownershipVerified: true, - cnameVerified: true, - targetCname: true, - verificationStatus: true, - }, - }); - - if (!domainRecord) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Domain not found", - }); - } - - // Return the current verification state from the database - // The actual DNS checks happen in the backend worker - return { - domain: domainRecord.domain, - verificationToken: domainRecord.verificationToken, - ownershipVerified: domainRecord.ownershipVerified, - cnameVerified: domainRecord.cnameVerified, - targetCname: domainRecord.targetCname, - verificationStatus: domainRecord.verificationStatus, - }; - }); diff --git a/web/apps/dashboard/lib/trpc/routers/index.ts b/web/apps/dashboard/lib/trpc/routers/index.ts index 5a5a533a0e..edcad46979 100644 --- a/web/apps/dashboard/lib/trpc/routers/index.ts +++ b/web/apps/dashboard/lib/trpc/routers/index.ts @@ -39,7 +39,6 @@ import { queryRoles } from "./authorization/roles/query"; import { upsertRole } from "./authorization/roles/upsert"; import { queryUsage } from "./billing/query-usage"; import { addCustomDomain } from "./deploy/custom-domains/add"; -import { checkDns } from "./deploy/custom-domains/check-dns"; import { deleteCustomDomain } from "./deploy/custom-domains/delete"; import { listCustomDomains } from "./deploy/custom-domains/list"; import { retryVerification } from "./deploy/custom-domains/retry"; @@ -416,7 +415,6 @@ export const router = t.router({ list: listCustomDomains, delete: deleteCustomDomain, retry: retryVerification, - checkDns: checkDns, }), deployment: t.router({ list: listDeployments,