Skip to content

Implement managed proxy support for Cloudflare custom hostnames#1030

Open
goldflag wants to merge 3 commits into
masterfrom
managed-proxy
Open

Implement managed proxy support for Cloudflare custom hostnames#1030
goldflag wants to merge 3 commits into
masterfrom
managed-proxy

Conversation

@goldflag

@goldflag goldflag commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator
  • Added environment variables for Cloudflare API integration in .env.example.
  • Introduced proxy management functionality in the API, including endpoints to enable, disable, and check the status of managed proxies for sites.
  • Updated site deletion logic to remove associated Cloudflare hostnames and proxy domains when a site is deleted.
  • Enhanced server logic to start and stop the Cloudflare reconciliation service and manage proxy domain refresh.
  • Updated database schema to include fields for proxy domain management, ensuring proper tracking and handling of custom hostnames.

Summary by CodeRabbit

  • New Features
    • Added managed-proxy support for sites, including admin endpoints to enable, fetch status, and disable a Cloudflare custom domain.
    • Added a “Custom Domain” tab in Site Settings with domain setup, verification state, DNS record guidance, and an install script snippet.
    • Added automatic proxy hostname routing and background Cloudflare reconciliation to keep custom hostnames in sync.
  • Bug Fixes
    • Improved best-effort teardown on site deletion and Stripe subscription cancellation to reduce orphaned proxy hostnames.
  • Chores
    • Updated the Cloudflare configuration example with managed-proxy environment variables.

- Added environment variables for Cloudflare API integration in `.env.example`.
- Introduced proxy management functionality in the API, including endpoints to enable, disable, and check the status of managed proxies for sites.
- Updated site deletion logic to remove associated Cloudflare hostnames and proxy domains when a site is deleted.
- Enhanced server logic to start and stop the Cloudflare reconciliation service and manage proxy domain refresh.
- Updated database schema to include fields for proxy domain management, ensuring proper tracking and handling of custom hostnames.
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rybbit Ready Ready Preview, Comment Jun 25, 2026 1:32pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af34b1d7-ca06-456c-bdac-e910e571b94b

📥 Commits

Reviewing files that changed from the base of the PR and between 2da63ae and 65d3cc6.

📒 Files selected for processing (1)
  • server/src/api/sites/proxy/getProxyStatus.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/api/sites/proxy/getProxyStatus.ts

📝 Walkthrough

Walkthrough

This PR adds Cloudflare-backed managed proxy support for sites: configuration and schema updates, Cloudflare client helpers, proxy enable/status/disable APIs, hostname rewriting, lifecycle reconciliation, and teardown paths for site deletion and Stripe subscription deletion.

Changes

Managed Proxy Support

Layer / File(s) Summary
Cloudflare env and site proxy schema
server/.env.example, server/src/db/postgres/schema.ts
Adds managed-proxy Cloudflare environment placeholders and extends sites with proxy fields plus a unique proxyDomain constraint.
Cloudflare API client
server/src/lib/cloudflare.ts
Adds Cloudflare custom-hostname types, request handling, and functions to create, fetch, list, and delete custom hostnames.
Proxy domain helpers and response shaping
server/src/api/sites/proxy/utils.ts, server/src/lib/proxyDomains.ts
Adds proxy-domain normalization, status mapping, DNS record generation, response building, in-memory domain tracking, and refresh controls.
Proxy enable, status, and disable handlers
server/src/api/sites/proxy/*, server/src/api/sites/index.ts
Adds managed-proxy enable/status/disable handlers, Cloudflare registration and teardown paths, and re-exports them from the sites API entrypoint.
Request routing and lifecycle wiring
server/src/index.ts, server/src/cluster.ts
Rewrites managed-proxy hostnames onto API routes, registers the proxy routes, and starts or stops proxy refresh and Cloudflare reconciliation during process lifecycle hooks.
Proxy reconciliation and teardown
server/src/services/cloudflare/cloudflareReconciliationService.ts, server/src/api/sites/deleteSite.ts, server/src/api/stripe/webhook.ts
Adds scheduled Cloudflare orphan reconciliation, Stripe subscription deletion teardown, and best-effort proxy cleanup during site deletion.

Sequence Diagram(s)

Proxy enablement

sequenceDiagram
  participant enableProxy
  participant createCustomHostname
  participant sitesTable
  participant proxyDomains

  enableProxy->>createCustomHostname: create Cloudflare custom hostname
  createCustomHostname-->>enableProxy: CustomHostname
  enableProxy->>sitesTable: update proxyDomain, proxyEnabled, proxyStatus, proxyCfHostnameId
  enableProxy->>proxyDomains: addProxyDomain(domain)
Loading

Cloudflare reconciliation

sequenceDiagram
  participant cloudflareReconciliationService
  participant sitesTable
  participant listCustomHostnames
  participant deleteCustomHostname
  participant proxyDomains

  cloudflareReconciliationService->>sitesTable: load live proxyDomain values
  cloudflareReconciliationService->>listCustomHostnames: list Cloudflare custom hostnames
  cloudflareReconciliationService->>deleteCustomHostname: delete orphan hostname
  cloudflareReconciliationService->>proxyDomains: removeProxyDomain(orphan.hostname)
Loading

Stripe teardown

sequenceDiagram
  participant teardownProxiesForStripeCustomer
  participant deleteCustomHostname
  participant sitesTable
  participant proxyDomains

  teardownProxiesForStripeCustomer->>deleteCustomHostname: delete stored Cloudflare custom hostname
  teardownProxiesForStripeCustomer->>sitesTable: clear proxyDomain, proxyEnabled, proxyStatus, proxyCfHostnameId
  teardownProxiesForStripeCustomer->>proxyDomains: removeProxyDomain(hostname)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through Cloudflare clouds so bright,
and sniffed new hostnames day and night.
I nibbled routes, then bounded free,
with proxy domains in harmony.
Hop hop hooray for sites and me!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.22% 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 and concisely summarizes the main change: adding managed proxy support for Cloudflare custom hostnames.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch managed-proxy

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.

@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: 11

🧹 Nitpick comments (2)
server/src/services/cloudflare/cloudflareReconciliationService.ts (1)

86-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the implicit any for cfHostnames.

let cfHostnames; drops strict typing on the main reconcile path. Please type the variable explicitly or keep the awaited result as a const inside the try.

Suggested fix
-    let cfHostnames;
+    let cfHostnames: Awaited<ReturnType<typeof listCustomHostnames>>;
     try {
       cfHostnames = await listCustomHostnames();

As per coding guidelines, **/*.{ts,tsx} should "Use TypeScript with strict typing throughout both client and server".

🤖 Prompt for 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.

In `@server/src/services/cloudflare/cloudflareReconciliationService.ts` around
lines 86 - 88, The main reconcile path in cloudflareReconciliationService is
losing strict typing because cfHostnames is declared without a type. Update the
listCustomHostnames handling in the reconciliation flow to either declare
cfHostnames with an explicit type or keep the awaited result as a const inside
the try block, so the Cloudflare reconciliation logic stays strictly typed and
avoids implicit any.

Source: Coding guidelines

server/src/api/sites/proxy/getProxyStatus.ts (1)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the Fastify request contract fully explicit.

This handler only declares Params, so request.query and request.body remain implicit. Use an explicit route type for all three shapes to stay aligned with the API typing rule. As per coding guidelines, "Type Fastify requests with explicit Params, Querystring, and Body shapes in route handlers."

🤖 Prompt for 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.

In `@server/src/api/sites/proxy/getProxyStatus.ts` at line 11, The getProxyStatus
route handler only types request.Params, leaving request.query and request.body
implicit; update the FastifyRequest contract on getProxyStatus to explicitly
declare Params, Querystring, and Body shapes. Use the existing getProxyStatus
symbol to locate the handler and keep the route typing aligned with the API
typing rule even if query/body are empty objects.

Source: Coding guidelines

🤖 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 `@server/src/api/sites/deleteSite.ts`:
- Around line 16-34: The managed-proxy teardown in deleteSite is happening
before the site deletion is durable, which can leave the site row live while the
Cloudflare hostname and in-memory proxy state are already removed. Move the
deleteCustomHostname and removeProxyDomain work in deleteSite to run only after
the DB delete has succeeded/committed, or otherwise make the DB deletion the
durable step before any external cleanup. Use the existing siteRow,
deleteCustomHostname, and removeProxyDomain flow as the anchor when relocating
the cleanup.

In `@server/src/api/sites/proxy/enableProxy.ts`:
- Around line 59-97: The domain conflict check in enableProxy is only advisory
and can race, so move to a reservation/locking step before
createCustomHostname() to ensure only one request can claim a proxyDomain at a
time. Use the existing enableProxy flow around the conflicting query,
createCustomHostname(), and the sites update to atomically reserve the domain in
the database first, then proceed with Cloudflare creation only after the
reservation succeeds; if reservation fails, return the conflict response without
making the external write.
- Around line 54-103: The enableProxy flow in enableProxy.ts is overwriting an
existing proxy configuration in place without cleaning up the previous one.
Before updating the sites row, check whether the current site already has a
different proxyDomain/proxyCfHostnameId, and if so tear down the old Cloudflare
hostname and remove the old domain from the in-process proxy state before
calling createCustomHostname, db.update(sites), and addProxyDomain(domain). If
domain swaps are not meant to be supported, add a guard that rejects updates
when an active proxy already exists instead of replacing it silently.

In `@server/src/api/sites/proxy/getProxyStatus.ts`:
- Around line 12-15: Reject partially numeric siteId values by replacing the
loose parseInt-based parsing in getProxyStatus with strict validation before any
database access. Update the request.params.siteId handling so only fully numeric
positive IDs are accepted, and keep the existing 400 response path for invalid
input. Make sure the stricter check is applied consistently in getProxyStatus
and any related site-ID reads or updates that rely on the parsed siteId.

In `@server/src/api/sites/proxy/utils.ts`:
- Line 32: The hostname validation in HOSTNAME_REGEX still allows labels that
start or end with -, so update the pattern used by isValidProxyDomain() in
utils.ts to reject any dot-separated label with a leading or trailing hyphen.
Make sure the validation catches cases like a.-proxy.example and
a.proxy-.example before they reach the Cloudflare path, so the handler returns a
400 instead of failing later.

In `@server/src/db/postgres/schema.ts`:
- Around line 105-108: The proxy-domain uniqueness constraint in the schema is
currently case-sensitive, so duplicate hostnames with different casing can slip
through. Update the `sites` table definition in `schema.ts` so the invariant is
enforced case-insensitively, either by normalizing `table.proxyDomain` at the
schema boundary or by changing `sites_proxy_domain_unique` to use a
case-insensitive expression/index; make sure the
`unique(...).on(table.proxyDomain)` constraint still clearly identifies the
`proxyDomain` field as the source of truth.

In `@server/src/index.ts`:
- Around line 515-520: The server starts listening before the proxy-domain set
is fully warmed, which lets `rewriteUrl` miss managed-proxy requests right after
restart. Update `startProxyDomainRefresh()` in `server/src/lib/proxyDomains.ts`
to await the initial `refreshProxyDomains()` before returning, then keep the
interval setup after that. In `server/src/index.ts`, ensure the startup sequence
awaits `startProxyDomainRefresh()` before `server.listen(...)` so the set is
ready before traffic is served.

In `@server/src/lib/cloudflare.ts`:
- Around line 76-101: cfFetch currently lets stalled requests and low-level
fetch failures escape without normalization. Update cfFetch to use an
AbortSignal timeout for the fetch call, then wrap the fetch in try/catch and
convert network TypeError and abort/DOMException failures into CloudflareError
with an appropriate status/message. Keep the existing Cloudflare API error
handling after a successful response, and use the cfFetch symbol to locate the
change.

In `@server/src/lib/proxyDomains.ts`:
- Around line 38-52: The refreshProxyDomains() rebuild can overwrite concurrent
in-process updates from addProxyDomain() and removeProxyDomain(). Update
refreshProxyDomains() so it does not blindly clear and repopulate proxyDomains
from a stale DB snapshot after async waits; instead add a freshness
guard/version check or merge strategy that preserves newer eager changes, and
keep rewriteUrl() relying on the live set. Use the proxyDomains, addProxyDomain,
removeProxyDomain, and refreshProxyDomains symbols to locate the fix.

In `@server/src/services/cloudflare/cloudflareReconciliationService.ts`:
- Around line 160-177: The managed-proxy reconciliation path currently skips
sites with no Cloudflare hostname id and deletes the hostname before clearing
the DB state. Update cloudflareReconciliationService’s proxied-site loop to
select all rows that are still marked as managed/proxied for the organization,
persist the disabled site state in sites first, and then best-effort call
deleteCustomHostname only when proxyCfHostnameId is present. Keep the fix
localized around the proxied query and the for-of loop so the DB always reflects
disabled status even if the Cloudflare delete fails or the id is missing.
- Around line 96-103: The safety guard in CloudflareReconciliationService only
aborts when the zone size exceeds MAX_DELETES_PER_RUN, so a populated zone with
live.size === 0 can still be fully wiped on smaller deployments. Update the
reconciliation check in cloudflareReconciliationService to always stop the sweep
whenever live is empty and cfHostnames has any entries, and keep the existing
logger.error message and early return behavior tied to that guard.

---

Nitpick comments:
In `@server/src/api/sites/proxy/getProxyStatus.ts`:
- Line 11: The getProxyStatus route handler only types request.Params, leaving
request.query and request.body implicit; update the FastifyRequest contract on
getProxyStatus to explicitly declare Params, Querystring, and Body shapes. Use
the existing getProxyStatus symbol to locate the handler and keep the route
typing aligned with the API typing rule even if query/body are empty objects.

In `@server/src/services/cloudflare/cloudflareReconciliationService.ts`:
- Around line 86-88: The main reconcile path in cloudflareReconciliationService
is losing strict typing because cfHostnames is declared without a type. Update
the listCustomHostnames handling in the reconciliation flow to either declare
cfHostnames with an explicit type or keep the awaited result as a const inside
the try block, so the Cloudflare reconciliation logic stays strictly typed and
avoids implicit any.
🪄 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: aa243cbc-3023-44f2-9dda-e30d32bd9bdd

📥 Commits

Reviewing files that changed from the base of the PR and between e0fc6a9 and 3db8e51.

📒 Files selected for processing (15)
  • server/.env.example
  • server/src/api/sites/deleteSite.ts
  • server/src/api/sites/index.ts
  • server/src/api/sites/proxy/disableProxy.ts
  • server/src/api/sites/proxy/enableProxy.ts
  • server/src/api/sites/proxy/getProxyStatus.ts
  • server/src/api/sites/proxy/index.ts
  • server/src/api/sites/proxy/utils.ts
  • server/src/api/stripe/webhook.ts
  • server/src/cluster.ts
  • server/src/db/postgres/schema.ts
  • server/src/index.ts
  • server/src/lib/cloudflare.ts
  • server/src/lib/proxyDomains.ts
  • server/src/services/cloudflare/cloudflareReconciliationService.ts

Comment on lines +16 to +34
// If this site had a managed proxy, tear down its Cloudflare hostname so we stop being
// billed. Best-effort: failures are logged and collected by the daily reconcile sweep,
// and must never block the site deletion itself.
const [siteRow] = await db
.select({ proxyDomain: sites.proxyDomain, proxyCfHostnameId: sites.proxyCfHostnameId })
.from(sites)
.where(eq(sites.siteId, Number(id)))
.limit(1);

if (siteRow?.proxyCfHostnameId && isCloudflareConfigured) {
try {
await deleteCustomHostname(siteRow.proxyCfHostnameId);
} catch (error) {
logger.error(error as Error, `Failed to delete Cloudflare hostname for deleted site ${id}; reconcile will retry`);
}
}
if (siteRow?.proxyDomain) {
removeProxyDomain(siteRow.proxyDomain);
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't tear down the proxy before the site delete succeeds.

This runs before the deletion work at Lines 36-46. If one of those calls fails after deleteCustomHostname(...) succeeds, the site row can remain live while its managed-proxy hostname has already been removed from Cloudflare and from memory. The reconcile job only deletes orphans; it will not recreate the missing hostname.

Move the Cloudflare/in-memory teardown after the site delete commits, or make the DB delete and proxy-state clear the durable step before any external delete.

🤖 Prompt for 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.

In `@server/src/api/sites/deleteSite.ts` around lines 16 - 34, The managed-proxy
teardown in deleteSite is happening before the site deletion is durable, which
can leave the site row live while the Cloudflare hostname and in-memory proxy
state are already removed. Move the deleteCustomHostname and removeProxyDomain
work in deleteSite to run only after the DB delete has succeeded/committed, or
otherwise make the DB deletion the durable step before any external cleanup. Use
the existing siteRow, deleteCustomHostname, and removeProxyDomain flow as the
anchor when relocating the cleanup.

Comment on lines +54 to +103
const site = await db.query.sites.findFirst({ where: eq(sites.siteId, siteId) });
if (!site) {
return reply.status(404).send({ success: false, error: "Site not found" });
}

// The hostname can back only one site. Reject if another site already claims it.
const conflicting = await db
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.proxyDomain, domain), ne(sites.siteId, siteId)))
.limit(1);
if (conflicting.length > 0) {
return reply.status(409).send({ success: false, error: "That domain is already in use by another site" });
}

// Register the custom hostname with Cloudflare. If it already exists (e.g. a leftover
// from a previous attempt), reuse it rather than failing.
let customHostname: CustomHostname;
try {
customHostname = await createCustomHostname(domain);
} catch (error) {
if (error instanceof CloudflareError && (error.code === 1406 || error.code === 1407)) {
const existing = await findCustomHostnameByName(domain);
if (!existing) {
logger.error(error, `Cloudflare reported ${domain} exists but it could not be found`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
customHostname = existing;
} else {
logger.error(error as Error, `Failed to create Cloudflare custom hostname for ${domain}`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
}

await db
.update(sites)
.set({
proxyDomain: domain,
proxyEnabled: true,
proxyStatus: mapCloudflareStatus(customHostname),
proxyCfHostnameId: customHostname.id,
updatedAt: new Date().toISOString(),
})
.where(eq(sites.siteId, siteId));

// Make the domain serve traffic immediately in this process; other processes pick it
// up on their next refresh.
addProxyDomain(domain);

return reply.status(200).send({ success: true, ...buildProxyResponse(domain, siteId, customHostname) });

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't overwrite an existing proxy without tearing the old one down.

If this site already has a different proxyDomain / proxyCfHostnameId, Lines 88-97 replace that state in place. The previous Cloudflare hostname is left behind, and the old domain stays routable in this process until the next refresh because only addProxyDomain(domain) runs here.

Low-risk guard if domain swaps are not intentionally supported
   const site = await db.query.sites.findFirst({ where: eq(sites.siteId, siteId) });
   if (!site) {
     return reply.status(404).send({ success: false, error: "Site not found" });
   }
+
+  if (site.proxyEnabled && site.proxyDomain && site.proxyDomain !== domain) {
+    return reply.status(409).send({
+      success: false,
+      error: "Disable the existing proxy before assigning a new domain",
+    });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const site = await db.query.sites.findFirst({ where: eq(sites.siteId, siteId) });
if (!site) {
return reply.status(404).send({ success: false, error: "Site not found" });
}
// The hostname can back only one site. Reject if another site already claims it.
const conflicting = await db
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.proxyDomain, domain), ne(sites.siteId, siteId)))
.limit(1);
if (conflicting.length > 0) {
return reply.status(409).send({ success: false, error: "That domain is already in use by another site" });
}
// Register the custom hostname with Cloudflare. If it already exists (e.g. a leftover
// from a previous attempt), reuse it rather than failing.
let customHostname: CustomHostname;
try {
customHostname = await createCustomHostname(domain);
} catch (error) {
if (error instanceof CloudflareError && (error.code === 1406 || error.code === 1407)) {
const existing = await findCustomHostnameByName(domain);
if (!existing) {
logger.error(error, `Cloudflare reported ${domain} exists but it could not be found`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
customHostname = existing;
} else {
logger.error(error as Error, `Failed to create Cloudflare custom hostname for ${domain}`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
}
await db
.update(sites)
.set({
proxyDomain: domain,
proxyEnabled: true,
proxyStatus: mapCloudflareStatus(customHostname),
proxyCfHostnameId: customHostname.id,
updatedAt: new Date().toISOString(),
})
.where(eq(sites.siteId, siteId));
// Make the domain serve traffic immediately in this process; other processes pick it
// up on their next refresh.
addProxyDomain(domain);
return reply.status(200).send({ success: true, ...buildProxyResponse(domain, siteId, customHostname) });
const site = await db.query.sites.findFirst({ where: eq(sites.siteId, siteId) });
if (!site) {
return reply.status(404).send({ success: false, error: "Site not found" });
}
if (site.proxyEnabled && site.proxyDomain && site.proxyDomain !== domain) {
return reply.status(409).send({
success: false,
error: "Disable the existing proxy before assigning a new domain",
});
}
// The hostname can back only one site. Reject if another site already claims it.
const conflicting = await db
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.proxyDomain, domain), ne(sites.siteId, siteId)))
.limit(1);
if (conflicting.length > 0) {
return reply.status(409).send({ success: false, error: "That domain is already in use by another site" });
}
// Register the custom hostname with Cloudflare. If it already exists (e.g. a leftover
// from a previous attempt), reuse it rather than failing.
let customHostname: CustomHostname;
try {
customHostname = await createCustomHostname(domain);
} catch (error) {
if (error instanceof CloudflareError && (error.code === 1406 || error.code === 1407)) {
const existing = await findCustomHostnameByName(domain);
if (!existing) {
logger.error(error, `Cloudflare reported ${domain} exists but it could not be found`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
customHostname = existing;
} else {
logger.error(error as Error, `Failed to create Cloudflare custom hostname for ${domain}`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
}
await db
.update(sites)
.set({
proxyDomain: domain,
proxyEnabled: true,
proxyStatus: mapCloudflareStatus(customHostname),
proxyCfHostnameId: customHostname.id,
updatedAt: new Date().toISOString(),
})
.where(eq(sites.siteId, siteId));
// Make the domain serve traffic immediately in this process; other processes pick it
// up on their next refresh.
addProxyDomain(domain);
return reply.status(200).send({ success: true, ...buildProxyResponse(domain, siteId, customHostname) });
🤖 Prompt for 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.

In `@server/src/api/sites/proxy/enableProxy.ts` around lines 54 - 103, The
enableProxy flow in enableProxy.ts is overwriting an existing proxy
configuration in place without cleaning up the previous one. Before updating the
sites row, check whether the current site already has a different
proxyDomain/proxyCfHostnameId, and if so tear down the old Cloudflare hostname
and remove the old domain from the in-process proxy state before calling
createCustomHostname, db.update(sites), and addProxyDomain(domain). If domain
swaps are not meant to be supported, add a guard that rejects updates when an
active proxy already exists instead of replacing it silently.

Comment on lines +59 to +97
// The hostname can back only one site. Reject if another site already claims it.
const conflicting = await db
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.proxyDomain, domain), ne(sites.siteId, siteId)))
.limit(1);
if (conflicting.length > 0) {
return reply.status(409).send({ success: false, error: "That domain is already in use by another site" });
}

// Register the custom hostname with Cloudflare. If it already exists (e.g. a leftover
// from a previous attempt), reuse it rather than failing.
let customHostname: CustomHostname;
try {
customHostname = await createCustomHostname(domain);
} catch (error) {
if (error instanceof CloudflareError && (error.code === 1406 || error.code === 1407)) {
const existing = await findCustomHostnameByName(domain);
if (!existing) {
logger.error(error, `Cloudflare reported ${domain} exists but it could not be found`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
customHostname = existing;
} else {
logger.error(error as Error, `Failed to create Cloudflare custom hostname for ${domain}`);
return reply.status(502).send({ success: false, error: "Failed to register domain with Cloudflare" });
}
}

await db
.update(sites)
.set({
proxyDomain: domain,
proxyEnabled: true,
proxyStatus: mapCloudflareStatus(customHostname),
proxyCfHostnameId: customHostname.id,
updatedAt: new Date().toISOString(),
})
.where(eq(sites.siteId, siteId));

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reserve the domain before creating the Cloudflare hostname.

The check on Lines 60-64 is only advisory. Two concurrent requests can both pass it and both call createCustomHostname(). If the DB later rejects one update, you've already performed a non-idempotent external write and leaked a custom hostname; if it doesn't, two sites can claim the same host.

🤖 Prompt for 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.

In `@server/src/api/sites/proxy/enableProxy.ts` around lines 59 - 97, The domain
conflict check in enableProxy is only advisory and can race, so move to a
reservation/locking step before createCustomHostname() to ensure only one
request can claim a proxyDomain at a time. Use the existing enableProxy flow
around the conflicting query, createCustomHostname(), and the sites update to
atomically reserve the domain in the database first, then proceed with
Cloudflare creation only after the reservation succeeds; if reservation fails,
return the conflict response without making the external write.

Comment on lines +12 to +15
const siteId = parseInt(request.params.siteId, 10);
if (isNaN(siteId) || siteId <= 0) {
return reply.status(400).send({ success: false, error: "Invalid site ID" });
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject partially numeric siteId values.

parseInt accepts prefixes, so /sites/12abc/proxy is treated as site 12. That lets this handler read or update the wrong row on Lines 17, 43, and 51. Parse the param strictly before the database call.

Suggested fix
-  const siteId = parseInt(request.params.siteId, 10);
-  if (isNaN(siteId) || siteId <= 0) {
+  const rawSiteId = request.params.siteId;
+  if (!/^[1-9]\d*$/.test(rawSiteId)) {
     return reply.status(400).send({ success: false, error: "Invalid site ID" });
   }
+  const siteId = Number(rawSiteId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const siteId = parseInt(request.params.siteId, 10);
if (isNaN(siteId) || siteId <= 0) {
return reply.status(400).send({ success: false, error: "Invalid site ID" });
}
const rawSiteId = request.params.siteId;
if (!/^[1-9]\d*$/.test(rawSiteId)) {
return reply.status(400).send({ success: false, error: "Invalid site ID" });
}
const siteId = Number(rawSiteId);
🤖 Prompt for 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.

In `@server/src/api/sites/proxy/getProxyStatus.ts` around lines 12 - 15, Reject
partially numeric siteId values by replacing the loose parseInt-based parsing in
getProxyStatus with strict validation before any database access. Update the
request.params.siteId handling so only fully numeric positive IDs are accepted,
and keep the existing 400 response path for invalid input. Make sure the
stricter check is applied consistently in getProxyStatus and any related site-ID
reads or updates that rely on the parsed siteId.

* A first-party hostname under the customer's own domain. Requires at least one dot
* (it must be a subdomain/host, not a bare label) and stays within DNS length limits.
*/
const HOSTNAME_REGEX = /^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})+$/;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject labels that start or end with -.

This pattern still accepts invalid hostnames like a.-proxy.example and a.proxy-.example. Those pass isValidProxyDomain(), then fail later at Cloudflare as a 502 instead of a 400 from this handler.

Suggested fix
-const HOSTNAME_REGEX = /^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})+$/;
+const HOSTNAME_REGEX =
+  /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const HOSTNAME_REGEX = /^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})+$/;
const HOSTNAME_REGEX =
/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
🤖 Prompt for 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.

In `@server/src/api/sites/proxy/utils.ts` at line 32, The hostname validation in
HOSTNAME_REGEX still allows labels that start or end with -, so update the
pattern used by isValidProxyDomain() in utils.ts to reject any dot-separated
label with a leading or trailing hyphen. Make sure the validation catches cases
like a.-proxy.example and a.proxy-.example before they reach the Cloudflare
path, so the handler returns a 400 instead of failing later.

Comment thread server/src/index.ts
Comment on lines +515 to 520
// Keep the managed-proxy hostname set warm for the rewriteUrl hook (request-serving
// process: workers, or this process in single-process mode).
startProxyDomainRefresh();

// Start the server first
await server.listen({ port: 3001, host: "0.0.0.0" });

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Warm the proxy-domain set before listening.

startProxyDomainRefresh() fires the initial refresh in the background and returns immediately, but rewriteUrl depends on that set for every managed-proxy request. Right after a restart, those requests can miss the rewrite and 404 until the async warm-up finishes.

Suggested direction
-    startProxyDomainRefresh();
+    await startProxyDomainRefresh();
// server/src/lib/proxyDomains.ts
export async function startProxyDomainRefresh(): Promise<void> {
  await refreshProxyDomains();

  if (!refreshTimer) {
    refreshTimer = setInterval(() => void refreshProxyDomains(), REFRESH_INTERVAL_MS);
    refreshTimer.unref?.();
  }
}
🤖 Prompt for 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.

In `@server/src/index.ts` around lines 515 - 520, The server starts listening
before the proxy-domain set is fully warmed, which lets `rewriteUrl` miss
managed-proxy requests right after restart. Update `startProxyDomainRefresh()`
in `server/src/lib/proxyDomains.ts` to await the initial `refreshProxyDomains()`
before returning, then keep the interval setup after that. In
`server/src/index.ts`, ensure the startup sequence awaits
`startProxyDomainRefresh()` before `server.listen(...)` so the set is ready
before traffic is served.

Comment on lines +76 to +101
async function cfFetch<T>(path: string, init?: RequestInit): Promise<CfEnvelope<T>> {
if (!isCloudflareConfigured) {
throw new CloudflareError("Cloudflare is not configured", 500);
}

const res = await fetch(`${API_BASE}${path}`, {
...init,
headers: {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
...(init?.headers || {}),
},
});

const body = (await res.json().catch(() => null)) as CfEnvelope<T> | null;

if (!body || !body.success) {
const firstError = body?.errors?.[0];
throw new CloudflareError(
firstError?.message || `Cloudflare API error (HTTP ${res.status})`,
res.status,
firstError?.code
);
}

return body;

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.

🩺 Stability & Availability | 🟠 Major

Add a timeout and normalize transport errors in cfFetch.

The current implementation lacks a request timeout and fails to catch network-level rejections. A stalled call can hang request handlers or the reconciliation loop, and raw fetch errors escape as generic uncaught rejections instead of CloudflareError. Wrap the fetch call in a try/catch block that propagates TypeError (network failures) and DOMException (abort signals) as CloudflareError instances, and enforce a reasonable timeout using AbortSignal.

Current code
async function cfFetch<T>(path: string, init?: RequestInit): Promise<CfEnvelope<T>> {
  if (!isCloudflareConfigured) {
    throw new CloudflareError("Cloudflare is not configured", 500);
  }

  const res = await fetch(`${API_BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${API_TOKEN}`,
      "Content-Type": "application/json",
      ...(init?.headers || {}),
    },
  });

  const body = (await res.json().catch(() => null)) as CfEnvelope<T> | null;

  if (!body || !body.success) {
    const firstError = body?.errors?.[0];
    throw new CloudflareError(
      firstError?.message || `Cloudflare API error (HTTP ${res.status})`,
      res.status,
      firstError?.code
    );
  }

  return body;
}
🤖 Prompt for 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.

In `@server/src/lib/cloudflare.ts` around lines 76 - 101, cfFetch currently lets
stalled requests and low-level fetch failures escape without normalization.
Update cfFetch to use an AbortSignal timeout for the fetch call, then wrap the
fetch in try/catch and convert network TypeError and abort/DOMException failures
into CloudflareError with an appropriate status/message. Keep the existing
Cloudflare API error handling after a successful response, and use the cfFetch
symbol to locate the change.

Comment on lines +38 to +52
export async function refreshProxyDomains(): Promise<void> {
try {
const rows = await db
.select({ proxyDomain: sites.proxyDomain })
.from(sites)
.where(and(eq(sites.proxyEnabled, true), isNotNull(sites.proxyDomain)));

// Rebuild from the DB snapshot. The query above is the only await; the swap below
// is synchronous, so no request can observe a half-built set.
proxyDomains.clear();
for (const row of rows) {
if (row.proxyDomain) {
proxyDomains.add(row.proxyDomain.toLowerCase());
}
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The refresh path can clobber eager add/remove updates.

If refreshProxyDomains() starts its query, then addProxyDomain() or removeProxyDomain() runs before the query resolves, Lines 47-52 rebuild the set from an older snapshot and undo the in-process change. That can temporarily drop a newly enabled domain or resurrect a just-disabled one until the next 60s poll, which breaks rewriteUrl() host matching.

One way to fence off stale refreshes
-const proxyDomains = new Set<string>();
+let proxyDomains = new Set<string>();
+let proxyDomainsVersion = 0;

 export function addProxyDomain(host: string): void {
   proxyDomains.add(host.toLowerCase());
+  proxyDomainsVersion += 1;
 }

 export function removeProxyDomain(host: string): void {
   proxyDomains.delete(host.toLowerCase());
+  proxyDomainsVersion += 1;
 }

 export async function refreshProxyDomains(): Promise<void> {
   try {
+    const versionAtStart = proxyDomainsVersion;
     const rows = await db
       .select({ proxyDomain: sites.proxyDomain })
       .from(sites)
       .where(and(eq(sites.proxyEnabled, true), isNotNull(sites.proxyDomain)));

-    proxyDomains.clear();
+    const nextProxyDomains = new Set<string>();
     for (const row of rows) {
       if (row.proxyDomain) {
-        proxyDomains.add(row.proxyDomain.toLowerCase());
+        nextProxyDomains.add(row.proxyDomain.toLowerCase());
       }
     }
+
+    if (versionAtStart === proxyDomainsVersion) {
+      proxyDomains = nextProxyDomains;
+    }
   } catch (error) {
🤖 Prompt for 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.

In `@server/src/lib/proxyDomains.ts` around lines 38 - 52, The
refreshProxyDomains() rebuild can overwrite concurrent in-process updates from
addProxyDomain() and removeProxyDomain(). Update refreshProxyDomains() so it
does not blindly clear and repopulate proxyDomains from a stale DB snapshot
after async waits; instead add a freshness guard/version check or merge strategy
that preserves newer eager changes, and keep rewriteUrl() relying on the live
set. Use the proxyDomains, addProxyDomain, removeProxyDomain, and
refreshProxyDomains symbols to locate the fix.

Comment on lines +96 to +103
// Defensive guard: an empty live set against a populated zone almost certainly means
// a bad read, not that every customer churned at once. Refuse to wipe the zone.
if (live.size === 0 && cfHostnames.length > this.MAX_DELETES_PER_RUN) {
this.logger.error(
`Reconciliation found 0 live hostnames but ${cfHostnames.length} in Cloudflare — aborting as a safety measure.`
);
return;
}

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Abort any populated-zone sweep when the live set is empty.

The current guard only aborts when Cloudflare has more than MAX_DELETES_PER_RUN hostnames. On a smaller deployment, one bad DB read that returns zero live rows would still delete every active hostname in the zone.

Suggested fix
-    if (live.size === 0 && cfHostnames.length > this.MAX_DELETES_PER_RUN) {
+    if (live.size === 0 && cfHostnames.length > 0) {
       this.logger.error(
         `Reconciliation found 0 live hostnames but ${cfHostnames.length} in Cloudflare — aborting as a safety measure.`
       );
       return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Defensive guard: an empty live set against a populated zone almost certainly means
// a bad read, not that every customer churned at once. Refuse to wipe the zone.
if (live.size === 0 && cfHostnames.length > this.MAX_DELETES_PER_RUN) {
this.logger.error(
`Reconciliation found 0 live hostnames but ${cfHostnames.length} in Cloudflare — aborting as a safety measure.`
);
return;
}
// Defensive guard: an empty live set against a populated zone almost certainly means
// a bad read, not that every customer churned at once. Refuse to wipe the zone.
if (live.size === 0 && cfHostnames.length > 0) {
this.logger.error(
`Reconciliation found 0 live hostnames but ${cfHostnames.length} in Cloudflare — aborting as a safety measure.`
);
return;
}
🤖 Prompt for 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.

In `@server/src/services/cloudflare/cloudflareReconciliationService.ts` around
lines 96 - 103, The safety guard in CloudflareReconciliationService only aborts
when the zone size exceeds MAX_DELETES_PER_RUN, so a populated zone with
live.size === 0 can still be fully wiped on smaller deployments. Update the
reconciliation check in cloudflareReconciliationService to always stop the sweep
whenever live is empty and cfHostnames has any entries, and keep the existing
logger.error message and early return behavior tied to that guard.

Comment on lines +160 to +177
const proxied = await db
.select({
siteId: sites.siteId,
proxyDomain: sites.proxyDomain,
proxyCfHostnameId: sites.proxyCfHostnameId,
})
.from(sites)
.where(and(inArray(sites.organizationId, orgIds), isNotNull(sites.proxyCfHostnameId)));

for (const site of proxied) {
try {
if (site.proxyCfHostnameId) {
await deleteCustomHostname(site.proxyCfHostnameId);
}
await db
.update(sites)
.set({ proxyDomain: null, proxyEnabled: false, proxyStatus: null, proxyCfHostnameId: null })
.where(eq(sites.siteId, site.siteId));

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Disable the site in the DB even when the Cloudflare hostname id is missing, and do that before the delete call.

This path only visits rows with proxyCfHostnameId, so any churned site stuck with proxyEnabled/proxyDomain but no hostname id is skipped entirely. It also deletes in Cloudflare before clearing sites, so a DB write failure leaves the site marked live even though the hostname is already gone.

Select all managed-proxy rows for the customer, persist the disabled state first, then best-effort delete the Cloudflare hostname when an id exists.

🤖 Prompt for 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.

In `@server/src/services/cloudflare/cloudflareReconciliationService.ts` around
lines 160 - 177, The managed-proxy reconciliation path currently skips sites
with no Cloudflare hostname id and deletes the hostname before clearing the DB
state. Update cloudflareReconciliationService’s proxied-site loop to select all
rows that are still marked as managed/proxied for the organization, persist the
disabled site state in sites first, and then best-effort call
deleteCustomHostname only when proxyCfHostnameId is present. Keep the fix
localized around the proxied query and the for-of loop so the DB always reflects
disabled status even if the Cloudflare delete fails or the id is missing.

- Added new translation keys and refined existing entries in Czech, German, English, Spanish, French, Italian, Japanese, and other languages to enhance user experience with API functionalities.
- Included translations for filters, parameters, and error handling, ensuring comprehensive coverage of new features related to API interactions and data management.
- Improved consistency and clarity across all supported languages, supporting the latest updates in the application.

@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: 2

🤖 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 `@client/src/components/SiteSettings/CustomDomainTab.tsx`:
- Around line 103-105: The CustomDomainTab state check is too broad and treats
an omitted configured field as false. Update the conditional in the
CustomDomainTab component to only enter the “not configured” branch when
configured is explicitly false, or otherwise make sure the API always returns
configured for valid responses. Use the existing data?.configured check in the
component to locate and tighten this logic.
- Line 4: The CustomDomainTab component is importing the wrong next-intl hook
for runtime user-facing text. Replace useExtracted with useTranslations in the
CustomDomainTab.tsx import and update the component to use the standard
useTranslations() hook for all translated strings, keeping the existing
translation keys and usage consistent.
🪄 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: d0910491-7dd8-413e-a5ed-88efcc358e86

📥 Commits

Reviewing files that changed from the base of the PR and between 3db8e51 and 2da63ae.

📒 Files selected for processing (17)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/api/admin/endpoints/index.ts
  • client/src/api/admin/endpoints/proxy.ts
  • client/src/api/admin/hooks/useProxy.ts
  • client/src/components/SiteSettings/CustomDomainTab.tsx
  • client/src/components/SiteSettings/SiteSettings.tsx
✅ Files skipped from review due to trivial changes (1)
  • client/src/api/admin/endpoints/index.ts

"use client";

import { Check, Copy, Globe, Loader2, RefreshCw, ShieldAlert } from "lucide-react";
import { useExtracted } from "next-intl";

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.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does next-intl v4.8.3 export a hook named useExtracted, and what is the recommended hook for component translations?

💡 Result:

Yes, next-intl v4.8.3 exports an experimental hook named useExtracted [1][2][3]. The recommended hook for component translations is useTranslations [4][5]. Additional context: The useExtracted hook is an experimental feature designed to automate message extraction from source files [1][2]. It integrates with the Next.js build process (via Turbopack or Webpack) to compile calls into useTranslations during development and build time [1][2]. Because it is experimental, it must be explicitly enabled in your next.config.ts file [1]. The standard and primary hook for consuming translations within React components remains useTranslations, which is the established API for most use cases [4][5][6].

Citations:


Replace useExtracted with useTranslations.

The project guidelines require using useTranslations() for user-facing strings. While useExtracted is an available experimental hook in next-intl, it is intended for build-time message extraction and is not the standard API for component translations.

import { useTranslations } from "next-intl";
🤖 Prompt for 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.

In `@client/src/components/SiteSettings/CustomDomainTab.tsx` at line 4, The
CustomDomainTab component is importing the wrong next-intl hook for runtime
user-facing text. Replace useExtracted with useTranslations in the
CustomDomainTab.tsx import and update the component to use the standard
useTranslations() hook for all translated strings, keeping the existing
translation keys and usage consistent.

Source: Coding guidelines

Comment on lines +103 to +105
// Feature isn't enabled on this server (no Cloudflare credentials configured).
if (!data?.configured) {
return (

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t treat missing configured as “not configured.”

if (!data?.configured) will show the “not configured” state when configured is omitted/undefined, even for valid proxy responses. Use an explicit check (data?.configured === false) or align the API contract to always send configured.

Suggested fix
-  if (!data?.configured) {
+  if (data?.configured === false) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Feature isn't enabled on this server (no Cloudflare credentials configured).
if (!data?.configured) {
return (
// Feature isn't enabled on this server (no Cloudflare credentials configured).
if (data?.configured === false) {
return (
🤖 Prompt for 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.

In `@client/src/components/SiteSettings/CustomDomainTab.tsx` around lines 103 -
105, The CustomDomainTab state check is too broad and treats an omitted
configured field as false. Update the conditional in the CustomDomainTab
component to only enter the “not configured” branch when configured is
explicitly false, or otherwise make sure the API always returns configured for
valid responses. Use the existing data?.configured check in the component to
locate and tighten this logic.

…uration details

- Updated the response structure to include `configured` and `cnameTarget` fields, providing clearer information about Cloudflare settings.
- Improved the handling of proxy status reporting, ensuring accurate responses based on the current configuration and status of the site.
- Refactored the logic to maintain consistency in the API response format, enhancing clarity for users interacting with the proxy status endpoint.
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