Skip to content

feat: custom org domains - UI, schema, and CRUD endpoints#27797

Open
Amit91848 wants to merge 12 commits intomainfrom
feat/custom-org-domains-ui
Open

feat: custom org domains - UI, schema, and CRUD endpoints#27797
Amit91848 wants to merge 12 commits intomainfrom
feat/custom-org-domains-ui

Conversation

@Amit91848
Copy link
Member

Add custom domain management for organizations:

  • CustomDomain Prisma model (1:1 with Team)
  • Vercel API integration for domain registration and verification
  • tRPC CRUD endpoints (add, get, verify, remove)
  • Settings page UI for managing custom domains
  • Migration for custom_domains table

What does this PR do?

  • Fixes #XXXX (GitHub issue number)
  • Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).

Video Demo (if applicable):

  • Show screen recordings of the issue or feature.
  • Demonstrate how to reproduce the issue, the behavior before and after the change.

Image Demo (if applicable):

  • Add side-by-side screenshots of the original and updated change.
  • Highlight any significant change(s).

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings
  • My PR is too large (>500 lines or >10 files) and should be split into smaller PRs

Add custom domain management for organizations:
- CustomDomain Prisma model (1:1 with Team)
- Vercel API integration for domain registration and verification
- tRPC CRUD endpoints (add, get, verify, remove)
- Settings page UI for managing custom domains
- Migration for custom_domains table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added the ❗️ migrations contains migration files label Feb 9, 2026
Copy link
Member Author

Amit91848 commented Feb 9, 2026

@Amit91848 Amit91848 mentioned this pull request Feb 9, 2026
3 tasks
@github-actions
Copy link
Contributor

E2E results are ready!

@Amit91848 Amit91848 marked this pull request as ready for review February 10, 2026 17:55
@Amit91848 Amit91848 requested a review from a team as a code owner February 10, 2026 17:55
@graphite-app graphite-app bot added the core area: core, team members only label Feb 10, 2026
Copy link
Contributor

@Udit-takkar Udit-takkar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove all the un necessary text from description and link the issue or thread? Also add screenshots/video and unit tests

@Udit-takkar Udit-takkar marked this pull request as draft February 12, 2026 15:08
Amit91848 and others added 7 commits February 13, 2026 18:03
Add custom domain management for organizations:
- CustomDomain Prisma model (1:1 with Team)
- Vercel API integration for domain registration and verification
- tRPC CRUD endpoints (add, get, verify, remove)
- Settings page UI for managing custom domains
- Migration for custom_domains table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Amit91848
Copy link
Member Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 13, 2026

@cubic-dev-ai

@Amit91848 I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 23 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/prisma/schema.prisma">

<violation number="1" location="packages/prisma/schema.prisma:3316">
P1: Rule violated: **Prevent Direct NOW() Usage in Database Queries**

Use a timezone-specified default instead of now() for new timestamp columns to avoid non-UTC inserts, per “Prevent Direct NOW() Usage in Database Queries.”</violation>
</file>

<file name="apps/web/modules/ee/organizations/custom-domain.tsx">

<violation number="1" location="apps/web/modules/ee/organizations/custom-domain.tsx:582">
P3: Localize this placeholder string with t() and add the corresponding translation key to the locale source so it can be managed by lingo.dev.</violation>

<violation number="2" location="apps/web/modules/ee/organizations/custom-domain.tsx:659">
P2: Removing the current domain before confirming the new domain is accepted can leave the org without a domain if the add mutation fails. Consider an atomic update (server-side replace) or ensure the new domain is reserved/added before removing the old one.</violation>
</file>

<file name="packages/lib/domainManager/deploymentServices/vercel.ts">

<violation number="1" location="packages/lib/domainManager/deploymentServices/vercel.ts:141">
P2: This module now validates/reads env vars directly. Prefer fetching Vercel config from the centralized config service (e.g., getEnv/ConfigService) so env parsing/validation happens once and callers receive normalized values.

(Based on your team's feedback about centralizing env-derived configuration.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/trpc/server/routers/viewer/organizations/_router.tsx">

<violation number="1" location="packages/trpc/server/routers/viewer/organizations/_router.tsx:243">
P2: verifyCustomDomain is defined as a query but it updates verification state in the database (and triggers external verification). This should be a mutation to avoid side effects in query handlers and prevent incorrect caching/GET semantics.</violation>
</file>

<file name="apps/web/modules/ee/organizations/appearance.tsx">

<violation number="1" location="apps/web/modules/ee/organizations/appearance.tsx:67">
P2: Keep the toggle state in sync with the query by resetting it to false when customDomain is cleared; otherwise the UI can stay enabled after a refetch that returns no domain.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +3316 to +3317
lastCheckedAt DateTime @default(now())
createdAt DateTime @default(now())
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Rule violated: Prevent Direct NOW() Usage in Database Queries

Use a timezone-specified default instead of now() for new timestamp columns to avoid non-UTC inserts, per “Prevent Direct NOW() Usage in Database Queries.”

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/prisma/schema.prisma, line 3316:

<comment>Use a timezone-specified default instead of now() for new timestamp columns to avoid non-UTC inserts, per “Prevent Direct NOW() Usage in Database Queries.”</comment>

<file context>
@@ -3305,3 +3307,17 @@ model RoutingTrace {
+  teamId        Int      @unique
+  slug          String   @unique
+  verified      Boolean  @default(false)
+  lastCheckedAt DateTime @default(now())
+  createdAt     DateTime @default(now())
+  updatedAt     DateTime @updatedAt
</file context>
Suggested change
lastCheckedAt DateTime @default(now())
createdAt DateTime @default(now())
lastCheckedAt DateTime @default(dbgenerated("timezone('utc', now())"))
createdAt DateTime @default(dbgenerated("timezone('utc', now())"))
Fix with Cubic


setIsUpdating(true);
try {
await removeMutation.mutateAsync({ teamId: orgId });
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Removing the current domain before confirming the new domain is accepted can leave the org without a domain if the add mutation fails. Consider an atomic update (server-side replace) or ensure the new domain is reserved/added before removing the old one.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/modules/ee/organizations/custom-domain.tsx, line 659:

<comment>Removing the current domain before confirming the new domain is accepted can leave the org without a domain if the add mutation fails. Consider an atomic update (server-side replace) or ensure the new domain is reserved/added before removing the old one.</comment>

<file context>
@@ -0,0 +1,777 @@
+
+    setIsUpdating(true);
+    try {
+      await removeMutation.mutateAsync({ teamId: orgId });
+      await addMutation.mutateAsync({ teamId: orgId, slug: domain });
+      showToast(t("custom_domain_updated"), "success");
</file context>
Fix with Cubic

};

export async function getDomain(domain: string): Promise<VercelDomainResponse> {
assertVercelEnvVars(process.env);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This module now validates/reads env vars directly. Prefer fetching Vercel config from the centralized config service (e.g., getEnv/ConfigService) so env parsing/validation happens once and callers receive normalized values.

(Based on your team's feedback about centralizing env-derived configuration.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/lib/domainManager/deploymentServices/vercel.ts, line 141:

<comment>This module now validates/reads env vars directly. Prefer fetching Vercel config from the centralized config service (e.g., getEnv/ConfigService) so env parsing/validation happens once and callers receive normalized values.

(Based on your team's feedback about centralizing env-derived configuration.) </comment>

<file context>
@@ -76,6 +137,67 @@ export const deleteDomain = async (domain: string) => {
 };
 
+export async function getDomain(domain: string): Promise<VercelDomainResponse> {
+  assertVercelEnvVars(process.env);
+  const normalizedDomain = domain.toLowerCase();
+
</file context>
Fix with Cubic

return handler(opts);
}),

verifyCustomDomain: authedProcedure.input(ZCustomDomainVerifyInputSchema).query(async (opts) => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: verifyCustomDomain is defined as a query but it updates verification state in the database (and triggers external verification). This should be a mutation to avoid side effects in query handlers and prevent incorrect caching/GET semantics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/trpc/server/routers/viewer/organizations/_router.tsx, line 243:

<comment>verifyCustomDomain is defined as a query but it updates verification state in the database (and triggers external verification). This should be a mutation to avoid side effects in query handlers and prevent incorrect caching/GET semantics.</comment>

<file context>
@@ -220,4 +224,24 @@ export const viewerOrganizationsRouter = router({
+    return handler(opts);
+  }),
+
+  verifyCustomDomain: authedProcedure.input(ZCustomDomainVerifyInputSchema).query(async (opts) => {
+    const { default: handler } = await import("./customDomain.verify.handler");
+    return handler(opts);
</file context>
Suggested change
verifyCustomDomain: authedProcedure.input(ZCustomDomainVerifyInputSchema).query(async (opts) => {
verifyCustomDomain: authedProcedure.input(ZCustomDomainVerifyInputSchema).mutation(async (opts) => {
Fix with Cubic

Comment on lines +67 to +71
useEffect(() => {
if (customDomain) {
setCustomDomainEnabled(true);
}
}, [customDomain]);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Keep the toggle state in sync with the query by resetting it to false when customDomain is cleared; otherwise the UI can stay enabled after a refetch that returns no domain.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/modules/ee/organizations/appearance.tsx, line 67:

<comment>Keep the toggle state in sync with the query by resetting it to false when customDomain is cleared; otherwise the UI can stay enabled after a refetch that returns no domain.</comment>

<file context>
@@ -53,6 +52,52 @@ const OrgAppearanceView = ({
+  const [customDomainEnabled, setCustomDomainEnabled] = useState(false);
+  const [showRemoveConfirmation, setShowRemoveConfirmation] = useState(false);
+
+  useEffect(() => {
+    if (customDomain) {
+      setCustomDomainEnabled(true);
</file context>
Suggested change
useEffect(() => {
if (customDomain) {
setCustomDomainEnabled(true);
}
}, [customDomain]);
useEffect(() => {
setCustomDomainEnabled(!!customDomain);
}, [customDomain]);
Fix with Cubic

type="text"
value={domain}
onChange={(e) => setDomain(e.target.value.toLowerCase().trim())}
placeholder="booking.yourdomain.com"
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Localize this placeholder string with t() and add the corresponding translation key to the locale source so it can be managed by lingo.dev.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/modules/ee/organizations/custom-domain.tsx, line 582:

<comment>Localize this placeholder string with t() and add the corresponding translation key to the locale source so it can be managed by lingo.dev.</comment>

<file context>
@@ -0,0 +1,777 @@
+              type="text"
+              value={domain}
+              onChange={(e) => setDomain(e.target.value.toLowerCase().trim())}
+              placeholder="booking.yourdomain.com"
+              className="border-default bg-default text-default placeholder:text-muted w-full rounded-md border px-3 py-2 text-sm focus:border-neutral-300 focus:outline-none focus:ring-0"
+              autoFocus
</file context>
Fix with Cubic

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

A Devin session has been created to address the issues identified by Cubic AI.

View Devin Session

@devin-ai-integration
Copy link
Contributor

I reviewed all 6 Cubic AI comments and checked their confidence scores. Here are the results:

# Issue Location Confidence
1 now() usage in schema packages/prisma/schema.prisma:3316 8/10
2 Localize placeholder string custom-domain.tsx:582 8/10
3 Remove before add race condition custom-domain.tsx:659 7/10
4 Centralize env vars vercel.ts:141 6/10
5 verifyCustomDomain query vs mutation _router.tsx:243 8/10
6 Toggle state sync with query appearance.tsx:67 7/10

No issues met the 9/10 confidence threshold for automated fixing. All 6 issues scored between 6/10 and 8/10. These should be reviewed manually by the team to determine which are worth addressing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only ❗️ migrations contains migration files ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants