Skip to content

feat(canvas): custom subdomain selection (Pro+) - #1727

Merged
2witstudios merged 5 commits into
masterfrom
feat/custom-subdomain-selection
Jun 26, 2026
Merged

2witstudios merged 5 commits into
masterfrom
feat/custom-subdomain-selection

Conversation

@2witstudios

@2witstudios 2witstudios commented Jun 25, 2026 •

Copy link
Copy Markdown
Owner

What

Pro+ users can now change their published canvas site's subdomain from the auto-allocated slug to a custom one via Drive Settings -> Domains & Publishing.

Changes

Area File Change
Plans plans.ts Added canChooseSubdomain to PlanDefinition.limits (false: free, true: pro/founder/business)
Backend publish-page.ts New changePublishSubdomain(): validates subdomain, checks uniqueness, updates drives.publishSubdomain, re-renders all published pages under new prefix, regenerates site files, cleans up old artifacts
API api/drives/[driveId]/subdomain/route.ts New endpoint: GET (status + canChange flag), PATCH (change subdomain with tier-gating)
UI domains/page.tsx New Subdomain card with live URL preview, Pro+ gating, upgrade CTA for free users
Test plans.test.ts Verify canChooseSubdomain gating across all tiers

Tier gating

  • Free: subdomain auto-allocated from drive slug, read-only display with upgrade CTA
  • Pro/Founder/Business: editable subdomain input with validation, live preview, and instant migration

Migration behavior

When a user changes their subdomain:

  1. New subdomain validated and checked for global uniqueness
  2. drives.publishSubdomain updated in DB
  3. All published pages re-rendered under the new prefix (reuses republishDriveCanonical)
  4. Site files (robots.txt, sitemap.xml, 404.html) regenerated
  5. Old artifacts deleted via clearPublishedPrefix (best-effort)

Security

  • Owner/admin only (both API and UI)
  • CSRF protected
  • Reserved subdomains blocked by existing validator

Notes

  • Could not run tsc --noEmit or tests in sandbox (no node_modules). CI should catch any type errors.
  • SKILL.md in PageSpace workspace updated separately

Summary by CodeRabbit

  • New Features

    • Added support for viewing and changing a published site’s custom subdomain.
    • The settings page now shows the current subdomain, a live preview URL, and upgrade messaging for unavailable tiers.
  • Bug Fixes

    • Improved validation and error handling when updating subdomains.
    • Ensures subdomain changes update published pages and related site files consistently.
  • Tests

    • Added coverage for tier-based subdomain availability.

- Add canChooseSubdomain to plan definitions (Pro+ only)
- Backend: changePublishSubdomain() validates, checks uniqueness,
  re-renders all published pages under new prefix, cleans up old artifacts
- API: PATCH /api/drives/[driveId]/subdomain with tier-gating
- UI: Subdomain card on Domains & Publishing settings
- Test: verify canChooseSubdomain gating across all tiers
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@2witstudios, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 10 minutes and 6 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4793e62d-81b2-4740-8c0d-2e36dfe1be2a

📥 Commits

Reviewing files that changed from the base of the PR and between 248298e and 1dbe4e3.

📒 Files selected for processing (3)
  • apps/web/src/app/api/drives/[driveId]/subdomain/route.ts
  • apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
  • apps/web/src/lib/canvas/publish-page.ts
📝 Walkthrough

Walkthrough

Adds drive publish subdomain management across the API, publishing workflow, plan limits, and dashboard settings. The new route reads and updates subdomains, the publish layer migrates published artifacts, and plan data now exposes subdomain eligibility.

Changes

Drive publish subdomain management

Layer / File(s) Summary
Tier subdomain limits
apps/web/src/lib/subscription/plans.ts, apps/web/src/lib/subscription/__tests__/plans.test.ts
canChooseSubdomain is added to plan limits, and the free, pro, founder, and business tiers are covered in tests.
Subdomain migration flow
apps/web/src/lib/canvas/publish-page.ts
changePublishSubdomain validates the candidate subdomain, checks uniqueness, updates the drive, republishes pages, regenerates site files, and clears old prefix artifacts.
Subdomain API endpoints
apps/web/src/app/api/drives/[driveId]/subdomain/route.ts
GET returns the current subdomain and change eligibility; PATCH enforces ownership, plan limits, JSON validation, mutation, and audit logging.
Dashboard subdomain editor
apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
The domains settings page fetches subdomain state, allows edits when permitted, submits updates, shows preview URLs, and links to billing when changes are unavailable.

Sequence Diagram(s)

sequenceDiagram
  participant SubdomainCard
  participant SubdomainRoute
  participant changePublishSubdomain
  participant publishedStorage
  SubdomainCard->>SubdomainRoute: GET /api/drives/[driveId]/subdomain
  SubdomainRoute->>SubdomainCard: subdomain + canChange
  SubdomainCard->>SubdomainRoute: PATCH subdomain
  SubdomainRoute->>changePublishSubdomain: changePublishSubdomain(driveId, subdomain, userId)
  changePublishSubdomain->>publishedStorage: republishDriveCanonical(...)
  changePublishSubdomain->>publishedStorage: clearPublishedPrefix(previousPrefix)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I hopped through subdomains, bright and neat,
With little code carrots and publishing feet.
A bunny can save, and the sites all align,
Then hop to the new URL, shiny and fine. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding custom subdomain selection for Pro+ canvas publishing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/custom-subdomain-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cafdf0eeec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/canvas/publish-page.ts Outdated
await db.update(drives).set({ publishSubdomain: normalized }).where(eq(drives.id, driveId));

// Re-render all published pages under the new subdomain prefix.
await republishDriveCanonical(driveId, userId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require successful migration before clearing old artifacts

When any published page fails to re-render during a subdomain change (for example an asset rewrite or publish-bucket upload error), republishDriveCanonical only logs the per-page failure and returns a count, but this caller ignores that result and proceeds to clear the old prefix. In that scenario the API reports success while some pages have no artifact under the new subdomain and the old working artifacts have been deleted, breaking the published site for those pages.

Useful? React with 👍 / 👎.

return NextResponse.json({ error: 'Only drive owners and admins can change the subdomain' }, { status: 403 });
}

if (!(await canChooseSubdomain(driveId))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Initialize the new plan limit before gating Pro users

For Pro and Founder drive owners this gate still rejects the request because the new limits.canChooseSubdomain value is not set on those plan objects, so getPlan(...).limits.canChooseSubdomain evaluates to undefined. That makes the advertised Pro+ feature available only to Business owners unless every tier gets an explicit value before this check is used.

Useful? React with 👍 / 👎.

</>
) : (
<p className="text-sm text-muted-foreground">
Custom subdomain selection is a Pro feature. <Link href="/dashboard/settings/billing" className="text-blue-500 hover:underline">Upgrade</Link> to choose your own subdomain.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Point the upgrade CTA at an existing settings route

For free users who click this Upgrade link, the target path appears to be a dead route: I checked the app routes and billing/plan pages live under /settings/billing and /settings/plan, with no /dashboard/settings/billing route or redirect. This sends the intended upgrade flow to a 404 instead of the billing UI.

Useful? React with 👍 / 👎.

- add canChooseSubdomain for free/pro/founder tiers
- fix missing publish-page imports (and/ne/clearPublishedPrefix)
- abort migration cleanup when republish count is incomplete
- map update unique-violation to 409
- fix plans.test bracket structure
- fix free-tier upgrade CTA route to /settings/billing
@2witstudios

Copy link
Copy Markdown
Owner Author

AIDD Reviewer pass completed and blockers addressed.

Applied in commit 248298e:

  • Fixed plan tier gating data:
    • canChooseSubdomain: false on Free
    • canChooseSubdomain: true on Pro, Founder, Business
  • Fixed compile blockers in publish-page.ts:
    • imported and, ne, clearPublishedPrefix
  • Hardened migration flow:
    • compare refreshedCount vs published row count
    • abort and return error on partial migration before old-prefix cleanup
  • Added unique-violation mapping on subdomain update to return 409 instead of generic 500
  • Fixed test syntax in plans.test.ts (removed extra });)
  • Fixed free-tier upgrade CTA route to /settings/billing

This should clear the CI blockers and satisfy the high-priority review comments.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/dashboard/`[driveId]/settings/domains/page.tsx:
- Around line 35-38: The subdomain settings UI is hard-coding the publish domain
instead of using the configured host, so update the SubdomainResponse and the
related domain settings flow to return and consume the actual publish host (for
example via url or publishHost) rather than reconstructing URLs with
pagespace.site. Use the existing domain settings components and API handling
around SubdomainResponse and the PATCH response to render the server-provided
host everywhere the publish URL is shown, including the configured card and any
success/response text.

In `@apps/web/src/lib/canvas/publish-page.ts`:
- Around line 503-505: The cleanup in publishPage can delete a prefix that may
already have been reclaimed by another drive once oldSubdomain is released.
Update the oldSubdomain cleanup flow in publishPage/clearPublishedPrefix so the
subdomain stays reserved until cleanup finishes, or guard it with a
per-subdomain lock or ownership check before deleting artifacts. Make sure the
logic around oldSubdomain and clearPublishedPrefix only removes artifacts proven
to belong to the current drive.
- Around line 477-497: The publishSubdomain update is committed in the
db.update(drives) step before republishing is confirmed, so a partial failure
leaves the drive pointing at an incomplete migration. In publish-page.ts, adjust
the publish flow around db.update, publishedPages.findMany, and
republishDriveCanonical so the subdomain change is only committed after
republishing succeeds, or add rollback/cleanup to restore the previous subdomain
when the refreshed count does not match. Keep the unique violation handling in
place and ensure the migration path leaves the drive in a consistent state on
any error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09f392d4-7c5a-4071-ab83-859927185e5e

📥 Commits

Reviewing files that changed from the base of the PR and between e201276 and 248298e.

📒 Files selected for processing (5)
  • apps/web/src/app/api/drives/[driveId]/subdomain/route.ts
  • apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
  • apps/web/src/lib/canvas/publish-page.ts
  • apps/web/src/lib/subscription/__tests__/plans.test.ts
  • apps/web/src/lib/subscription/plans.ts

Comment thread apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
Comment thread apps/web/src/lib/canvas/publish-page.ts Outdated
Comment thread apps/web/src/lib/canvas/publish-page.ts Outdated
Sprite added 3 commits June 26, 2026 00:30
… safety

- return publishHost from subdomain API and use it in settings UI
- stop hard-coding pagespace.site in subdomain card preview/links
- add rollback of drives.publishSubdomain when migration fails
- avoid old-prefix deletion race by removing prefix cleanup step
@2witstudios

Copy link
Copy Markdown
Owner Author

Addressed latest review comments in commit 1dbe4e3d.

What changed:

  • Publish host correctness
    • Subdomain API now returns publishHost on GET/PATCH.
    • Domains settings subdomain card now renders links/preview using publishHost instead of hard-coded pagespace.site.
  • Migration safety
    • changePublishSubdomain now rolls back drives.publishSubdomain to the previous value when migration fails.
    • Removed old-prefix deletion step to avoid the race where a reclaimed subdomain could have its artifacts deleted.

Files updated:

  • apps/web/src/app/api/drives/[driveId]/subdomain/route.ts
  • apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
  • apps/web/src/lib/canvas/publish-page.ts

@2witstudios
2witstudios merged commit 7e9e748 into master Jun 26, 2026
10 checks passed
@2witstudios
2witstudios deleted the feat/custom-subdomain-selection branch August 14, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant