Rovenue SDK
Funnels

Conversion

Changing a funnel's target — preview what the other target cannot carry, then convert.

Converting moves a funnel to the other target. It is the one action that changes target (apps/api/src/routes/dashboard/funnels.ts:873) — a PATCH cannot; see Targets. Nothing is rewritten silently: the server first says what the other target cannot carry, and the dashboard shows that answer and nothing else (apps/dashboard/src/components/funnel-builder/conversion.ts:9-13) — there is no capability list on the dashboard side to consult, on purpose.

The preview: what would be dropped

GET /dashboard/projects/:projectId/funnels/:funnelId/convert?target= (apps/api/src/routes/dashboard/funnels.ts:896) judges and changes nothing. It answers the conversion plan plus a slug suggestion (apps/api/src/routes/dashboard/funnels.ts:917):

{
  "data": {
    "from": "web",
    "to": "native",
    "blockers": [],
    "publishedIssues": [],
    "unpublishes": false,
    "stopsServing": null,
    "slug": null
  }
}

from and to name the direction (apps/api/src/routes/dashboard/funnels.ts:289-290). blockers is what the new target cannot carry — non-empty means the conversion is refused (apps/api/src/routes/dashboard/funnels.ts:237, apps/api/src/routes/dashboard/funnels.ts:291). publishedIssues is what the published version carries that the new target cannot — reported, never refused (apps/api/src/routes/dashboard/funnels.ts:292; the draft is always judged and always blocks, the published version never blocks — apps/api/src/routes/dashboard/funnels.ts:283-286). unpublishes is true when the funnel is live, so converting takes it off the air, and stopsServing names the slug whose public URL stops (apps/api/src/routes/dashboard/funnels.ts:293-294). slug is a suggestion only — offered so the confirmation can show the author the URL before it exists, while POST mints its own when none is sent (apps/api/src/routes/dashboard/funnels.ts:911-916); it is set only when converting to web from another target, and null otherwise (apps/api/src/routes/dashboard/funnels.ts:913-916).

Each entry carries the judge's fields — code, severity, pageId, blockId, message (packages/shared/src/funnel/issue.ts:28-34) — plus which document holds it (source: draft or published) and the page number counted in that document's own page list, 1-based, or null when the issue is about the whole funnel (apps/api/src/services/funnel/conversion.ts:52-55, apps/dashboard/src/lib/services/funnel-api.ts:65-68).

The list is a difference, not a second verdict: the same document is judged twice — once as it is, once as it would be — and only the errors the new target introduces are kept (apps/api/src/services/funnel/conversion.ts:94-106). A half-built draft already carrying an error on both targets is not something the conversion broke, so refusing over it would make converting wait on finishing.

The conversion itself

POST /dashboard/projects/:projectId/funnels/:funnelId/convert (apps/api/src/routes/dashboard/funnels.ts:921) takes target, an optional slug, and an optional unpublish acknowledgement (apps/api/src/routes/dashboard/funnels.ts:190-205). It refuses when there is nothing to do or something to fix first:

  • already on the target: 400 FUNNEL_ALREADY_ON_TARGET (apps/api/src/routes/dashboard/funnels.ts:946-950);
  • a slug toward native: 400, since a native funnel has no URL (apps/api/src/routes/dashboard/funnels.ts:932-934); toward web the slug is the author's, or one minted the way a new funnel's is (apps/api/src/routes/dashboard/funnels.ts:971-974);
  • draft blockers: 400 FUNNEL_CONVERSION_BLOCKED carrying the issues (apps/api/src/routes/dashboard/funnels.ts:953-956, apps/api/src/routes/dashboard/funnels.ts:208);
  • a live funnel without unpublish: true: 409 FUNNEL_CONVERSION_UNPUBLISHES naming what stops serving (apps/api/src/routes/dashboard/funnels.ts:958-964, apps/api/src/routes/dashboard/funnels.ts:209).

What survives: everything except the target and the address. The write patch is { target, slug } — plus status: "draft" when the conversion unpublishes (apps/api/src/routes/dashboard/funnels.ts:975-979). Screens, theme and settings are untouched, and the published version is kept: history and revert still have it, and revert restores it as a draft that the next publish re-judges (apps/api/src/routes/dashboard/funnels.ts:884-886). The conversion is audited as funnel.converted (apps/api/src/routes/dashboard/funnels.ts:988), the published-config cache for the stopped slug is invalidated (apps/api/src/routes/dashboard/funnels.ts:1017), and the response is the converted row (apps/api/src/routes/dashboard/funnels.ts:1019).

The cost is real: a visitor part-way through the live version cannot finish it (apps/api/src/routes/dashboard/funnels.ts:889-890). The result is a draft on the new target — nothing is served until a publish, which judges the draft under the target it will actually run as (apps/api/src/routes/dashboard/funnels.ts:881-884).

What the author must fix

A non-empty blockers list is the refusal itself (apps/dashboard/src/lib/services/funnel-api.ts:74). The dialog says it plainly — remove or replace the listed items first, and nothing has changed (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:121-122). Each blocker is shown with where it was found and its message (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:126-140); the page number is the server's, counted in the document the issue was found in, so a published version's page 2 is not whatever the draft has second (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:22-26). The published version's issues appear separately — they stop with the live version, and the draft does not have them (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:154-164); when nothing is being served they are a notice about what a restore would face (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:166-176).

The dashboard dialog

The builder names where the funnel runs and the one way to change it: the target was chosen at creation, and converting checks every page against what the other side can show first (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:60-70). The dialog moves through saving the latest changes, checking every page, showing the plan, and converting (apps/dashboard/src/components/funnel-builder/conversion.ts:44-52). Toward the web it asks for the address the funnel will be served at (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:178-188); for a live funnel the confirm button reads "Unpublish and convert" (apps/dashboard/src/components/funnel-builder/conversion-dialog.tsx:207-215). A failed conversion is read off the refusal code the server puts in the error message's JSON — blocked, unpublishes, or slug taken (apps/dashboard/src/components/funnel-builder/conversion.ts:15-20, apps/dashboard/src/components/funnel-builder/conversion.ts:84-106).

On this page