- Added support for connecting, refreshing, and removing custom domains through Cloudflare. - Updated API routes and validation schemas to handle new domain actions. - Refactored domain management logic in the dashboard to accommodate Cloudflare integration. - Enhanced user experience with clear status messages and DNS instructions for domain setup. - Updated translations and UI components to reflect new domain management features.
Cloudflare Custom Subdomain Reconnect PRD
Status
- Draft
Date
- March 22, 2026
Purpose
Replace the current Vercel-managed custom domain setup flow with a Cloudflare-managed custom subdomain flow, and require existing customers to reconnect their custom domains through the new setup path.
Problem Statement
Custom-domain setup is currently tightly coupled to Vercel project-domain APIs. The backend route in apps/api/src/routes/domain/domain.ts proxies directly to Vercel-specific actions, and the dashboard UI in apps/dashboard/src/lib/features/settings/pages/domains.svelte hardcodes Vercel DNS guidance (cname.vercel-dns.com).
This creates three problems:
- Domain provisioning logic is provider-specific instead of product-specific.
- The dashboard cannot render provider-agnostic DNS instructions or richer setup states.
- Existing setup assumes Vercel remains the control plane for custom domains, which no longer matches the intended infrastructure direction.
We are not doing an in-place live migration of existing Vercel-managed domains. Instead, organizations will be asked to set up their custom domain again through a new Cloudflare-backed flow.
Confirmed Decisions
- Cloudflare will manage custom-domain setup going forward.
- The app itself will not be served by Cloudflare as a full-app hosting migration in this project.
- Scope is limited to custom subdomains only.
- Apex/root domains are out of scope.
- Existing customers will reconnect their domain through the new flow instead of being transparently migrated from Vercel.
Goals
- Replace Vercel-specific custom-domain setup with a Cloudflare-backed setup flow.
- Keep the existing organization data model centered on
customDomainandisCustomDomainVerified. - Move domain orchestration into the backend so the dashboard does not manage provider sequencing itself.
- Render DNS instructions from API responses instead of hardcoded provider copy.
- Preserve runtime host-to-organization lookup for verified custom domains.
- Provide a clear reset and reconnect path for organizations with previously configured domains.
Non-Goals
- Supporting apex/root domains.
- Running the main app on Cloudflare.
- Preserving old Vercel domain attachments indefinitely.
- Building a multi-provider long-term abstraction unless required for rollout safety.
- Automatic migration of existing Vercel domain config without user action.
Data Sources Checked
apps/api/src/routes/domain/domain.tsapps/api/src/services/org/domain.tsapps/dashboard/src/lib/features/settings/pages/domains.svelteapps/dashboard/src/lib/features/org/api/org.svelte.tsapps/dashboard/src/lib/utils/functions/domain.tsapps/dashboard/src/lib/utils/services/org/domain.tsapps/dashboard/src/lib/utils/types/org.tsapps/dashboard/src/lib/features/app/layout-setup.tsapps/dashboard/src/lib/features/org/api/org.server.tsapps/docs/content/docs/how-to-guides/customize-organization.mdx
Current State
Backend
POST /domainacceptsverify_domain,add_domain, andremove_domain.- Those actions proxy directly to Vercel in
apps/api/src/services/org/domain.ts. - The backend contract is provider-shaped rather than product-shaped.
Dashboard
- The settings page stores
customDomainfirst via organization update, then separately calls the domain route. - DNS instructions are hardcoded to
CNAME -> cname.vercel-dns.com. - Verification UX is reduced to a simple pending/verified state with no provider-neutral detail.
Runtime
- Runtime org resolution already supports
customDomainlookups inapps/dashboard/src/lib/features/app/layout-setup.ts. - Server-side org fetchers already support
customDomain + isCustomDomainVerified. - This means the main runtime routing model can stay mostly intact if Cloudflare forwards the original host correctly.
Docs
- User-facing docs still say external custom domains are “coming soon”, which no longer matches the in-app paid feature.
User Story
As an organization admin, I want to connect a branded subdomain like courses.example.com to ClassroomIO through a clear Cloudflare-backed flow, so learners can access my organization site on my own domain.
As an existing organization admin with a previously configured domain, I want to understand that I must reconnect the domain through the new setup flow, and I want the product to show me the exact DNS record I need to add.
Functional Requirements
- Custom domain setup must accept only valid subdomains.
- The API must return normalized domain setup data instead of Vercel-specific payloads.
- The backend must own the full provisioning sequence:
- validate input
- provision/register domain with Cloudflare
- persist organization domain state
- return DNS instructions and current status
- The dashboard must render DNS instructions from API data.
- The dashboard must support at least these states:
- no domain configured
- reconnect required
- pending DNS
- pending verification
- verified
- error
- The dashboard must allow admins to remove a configured domain.
- Existing domains that were previously configured through Vercel must be markable as needing reconnection.
- Host-based org lookup must continue to work for verified custom domains.
- Free-plan gating for custom domains must remain unchanged unless a separate pricing decision changes it.
Non-Functional Requirements
- Domain-related errors must be actionable and provider-neutral in UI copy.
- Domain setup operations must be idempotent enough for safe retries by admins.
- Partial state must be minimized; backend orchestration should avoid “saved in DB but not provisioned” behavior.
- Rollout must allow safe validation in staging before production reset/reconnect begins.
Product Requirements
1) Setup Scope
- Accept only subdomains such as
courses.example.comorwww.example.com. - Reject apex domains such as
example.com. - Continue rejecting
*.classroomio.comas a custom domain.
2) Reconnect Requirement
- Existing organizations with
customDomainset under the old system must not be treated as automatically healthy. - The product should clearly indicate that custom domains must be reconnected through the new Cloudflare flow.
- A one-time reset/backfill step may mark existing domains as unverified or reconnect-required.
3) Backend-Owned Orchestration
The system should move from this sequence:
- dashboard updates organization record
- dashboard calls domain provider API
To this sequence:
- dashboard submits desired domain
- backend validates and provisions the domain
- backend updates organization state
- backend returns DNS instructions and status
4) DNS Instruction Model
The API should return a product-owned response shape, for example:
statusverifiedhostnamednsRecords[]messagereconnectRequired
The dashboard must not hardcode provider DNS values.
5) Verification Model
The system should support a refresh/check action that re-queries current provider state and updates organization verification status.
Minimum statuses:
pending_dnspending_verificationverifiederror
6) Remove Domain
Removing a domain should:
- remove or deactivate provider-side setup where applicable
- clear organization
customDomain - clear verification state
UX Requirements
- The settings page must clearly tell admins that only subdomains are supported.
- Existing domains that need reconnection must show a clear callout instead of silently appearing valid.
- DNS records must be copyable from the UI.
- Status badges and refresh actions must remain available after setup begins.
- User-facing copy must move through dashboard translations and not be hardcoded in component markup.
Technical Approach
Recommended Direction
Use a Cloudflare-backed service layer for custom-domain operations and expose a provider-neutral API contract to the dashboard.
Backend Changes
- Replace Vercel-specific service functions in
apps/api/src/services/org/domain.ts. - Refactor
apps/api/src/routes/domain/domain.tsso route responses are normalized around ClassroomIO product states, not Vercel response shapes. - Move organization-state persistence into backend orchestration rather than split frontend/backend sequencing.
- Add any required Cloudflare env configuration in API config.
Dashboard Changes
- Update
apps/dashboard/src/lib/features/settings/pages/domains.svelteto consume normalized API responses. - Remove hardcoded Vercel DNS values.
- Replace any Vercel-specific types in
apps/dashboard/src/lib/utils/types/org.ts. - Remove or refactor Vercel-specific helpers in
apps/dashboard/src/lib/utils/services/org/domain.ts. - Keep request typing in feature
utils/types.tsconsistent with existing dashboard conventions.
Runtime Changes
- Keep
apps/dashboard/src/lib/features/app/layout-setup.tsas the primary host-to-org resolver. - Verify that custom domains routed through the new provider path still preserve host-based organization lookup.
- No large routing-model rewrite is expected in this project.
Current State Audit
| Area | Current State | Gap | Priority |
|---|---|---|---|
| API domain setup | Vercel-specific proxy route | Needs Cloudflare-backed product contract | P0 |
| Backend orchestration | Frontend updates org, then provisions domain | Partial-state risk | P0 |
| Dashboard DNS instructions | Hardcoded Vercel CNAME target | Must become API-driven | P0 |
| Verification states | Basic pending/verified only | Needs richer reconnect/pending/error model | P0 |
| Existing domain rollout | Assumes current domain remains valid | Need reset + reconnect plan | P0 |
| Runtime org lookup | Already supports verified custom domains | Needs validation only, not redesign | P1 |
| Docs | External custom domains marked “coming soon” | Needs correction | P1 |
Implementation Plan
Phase 1: API Contract + Cloudflare Service
- Replace Vercel-specific service implementation in
apps/api/src/services/org/domain.ts. - Redesign
POST /domainresponse shape to be provider-neutral. - Implement backend orchestration so setup and persistence happen together.
- Add backend validation to enforce subdomain-only custom domains.
Phase 2: Dashboard Reconnect UX
- Update the domains settings page to render DNS instructions from API responses.
- Add reconnect-required state for existing domains.
- Update remove and refresh flows to use the new backend contract.
- Remove Vercel-specific helper/types that no longer apply.
Phase 3: Existing Domain Reset
- Identify organizations with existing
customDomainvalues. - Decide reset behavior:
- mark all as unverified
- add reconnect-required marker in UI
- optionally notify admins out-of-band
- Validate that old Vercel-managed domains no longer appear healthy in the product by default.
Phase 4: Docs + Rollout
- Update docs to describe the Cloudflare-backed custom-domain flow.
- Align in-app copy with product behavior.
- Test in staging with at least one reconnect scenario and one fresh setup scenario.
- Roll out production reset/reconnect.
Required File Touchpoints
Core API
apps/api/src/routes/domain/domain.tsapps/api/src/services/org/domain.tsapps/api/src/config/env.tsor related provider config filesapps/api/src/services/organization.tsif orchestration is moved into org service layer
Dashboard
apps/dashboard/src/lib/features/settings/pages/domains.svelteapps/dashboard/src/lib/features/org/api/org.svelte.tsapps/dashboard/src/lib/features/org/utils/types.tsapps/dashboard/src/lib/utils/functions/domain.tsapps/dashboard/src/lib/utils/services/org/domain.tsapps/dashboard/src/lib/utils/types/org.tsapps/dashboard/src/lib/utils/translations/en.json
Runtime Validation
apps/dashboard/src/lib/features/app/layout-setup.tsapps/dashboard/src/lib/features/org/api/org.server.ts
Docs
apps/docs/content/docs/how-to-guides/customize-organization.mdx
Rollout Plan
- Build the Cloudflare-backed flow behind a feature flag if needed.
- Validate fresh setup and reconnect flows in staging.
- Reset existing production custom domains into reconnect-required state.
- Publish updated docs and product copy.
- Ask affected organizations to reconnect their domains.
- Remove stale Vercel-specific setup code after production rollout stabilizes.
Testing Strategy
Unit Tests
- Domain input normalization and subdomain validation.
- Backend mapping from provider responses to product statuses.
- Reconnect-required state handling.
Integration Tests
- Add domain returns DNS instructions and a pending status.
- Refresh/verify updates
isCustomDomainVerifiedwhen provider verification succeeds. - Remove domain clears organization state correctly.
- Verified custom domain still resolves organization context via host-based lookup.
Manual Verification
- Fresh org connects a new subdomain successfully.
- Existing org with previously configured domain is shown reconnect-required state.
- DNS instructions shown in UI match backend response.
- Verified domain loads the correct organization site.
- Removing the domain clears status and UI state cleanly.
Acceptance Criteria
- The system no longer depends on Vercel project-domain APIs for custom-domain setup.
- Only subdomains are accepted for custom domains.
- The dashboard no longer hardcodes
cname.vercel-dns.com. - Existing domains can be explicitly reset into a reconnect-required flow.
- Verified custom domains continue to resolve to the correct organization site.
- Docs and in-app copy accurately describe the supported custom-domain flow.
Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| Backend/provider mismatch causes false verification state | Broken domain UX | Normalize provider states and add integration coverage |
| Existing domains appear valid after rollout even though reconnect is required | Admin confusion and broken trust | Reset state explicitly and surface reconnect-required UI |
| Host forwarding differs from current assumptions | Verified domains fail to resolve org context | Validate host preservation in staging before rollout |
| Frontend/backend sequencing remains split | Partial failures persist | Move orchestration into backend before release |
| Docs lag behind rollout | Support burden | Update docs in same release |
Open Questions
- Do we need a new persisted field for reconnect-required, or is resetting
isCustomDomainVerified=falsewith derived UI state sufficient? - Should production rollout notify affected organizations by email, in-app banner, or both?
- Do we want to keep the existing
/domainendpoint shape and only change payloads, or introduce a clearer route contract now?