Rovenue SDK
Funnels

Targets

A funnel is built for one target — web or native — and each target forbids what it cannot serve.

Every funnel is created for exactly one target: web or native. The target decides what the funnel may contain: store purchase blocks live on native funnels only, the Stripe checkout action and the express checkout block live on web funnels only, and everything else exists on both.

Fixed at creation

The create body carries an optional target (apps/api/src/routes/dashboard/funnels.ts:161), which is stored on the row (apps/api/src/routes/dashboard/funnels.ts:426). The update body has no target field at all — only expected_target (apps/api/src/routes/dashboard/funnels.ts:221), the target the builder tab believes the funnel has. A tab left editing a document for the old target after a conversion elsewhere gets a 409 FUNNEL_TARGET_CHANGED (apps/api/src/routes/dashboard/funnels.ts:267, apps/api/src/routes/dashboard/funnels.ts:487-488) instead of writing, so it stops saving and says so rather than failing every autosave.

The one action that changes target is the guarded conversion (apps/api/src/routes/dashboard/funnels.ts:321), through POST …/funnels/:funnelId/convert (apps/api/src/routes/dashboard/funnels.ts:1015) — see Conversion. A PATCH can never do it: the field simply is not there.

The web path

A web funnel is published to a URL. It gets a slug — the author's, or one minted from the name (apps/api/src/routes/dashboard/funnels.ts:175, apps/api/src/routes/dashboard/funnels.ts:182); a web row without one is refused by the funnels_web_requires_slug check (apps/api/src/routes/dashboard/funnels.ts:249-251). Once published it is served at that slug, scoped to its project — see Hosting. The public loader (GET /public/projects/:projectId/funnels/:slug) resolves by project and slug together and answers only for target = 'web' (apps/api/src/routes/public/funnels.ts:941, apps/api/src/routes/public/funnels.ts:945): a converted-away row keeps its history, but no web URL reaches it anymore.

Selling on a web funnel goes through Stripe: the publish route demands an account that can take a card of a web funnel with a checkout, and refuses with STRIPE_NOT_CONNECTED otherwise (apps/api/src/routes/dashboard/funnels.ts:771, apps/api/src/routes/dashboard/funnels.ts:772). A native funnel is never asked — its purchase goes through the store.

The native path

A native funnel has no URL. It is stored with a NULL slug (apps/api/src/routes/dashboard/funnels.ts:181) — a slug invented for it would be a web address nothing serves — and writing one onto it is an explicit 400 (apps/api/src/routes/dashboard/funnels.ts:499-500).

Instead it is served to a placement inside the app: a placement resolves to the published native funnel, served under the funnel envelope key (apps/api/src/lib/placement-resolution.ts:74, apps/api/src/lib/placement-resolution.ts:280, apps/api/src/lib/placement-resolution.ts:306-309). The envelope carries the row id, which the device stamps into its funnelContext, because the slug can be null for native funnels (apps/api/src/lib/placement-resolution.ts:49-52).

What each target forbids

The table below is generated from the one authority both the validator and the editor's palette read — capabilities(target) in @rovenue/funnel-core-wasm (packages/funnel-core-rs/src/capabilities.rs). Do not edit it by hand; run pnpm --filter @rovenue/docs generate:capabilities after changing the crate.

CapabilityKindOnly onWhy
checkoutactionwebPaying on this page happens through Stripe in the browser, so this is available on web funnels only.
expressCheckoutblockwebPaying on this page happens through Stripe in the browser, so this is available on web funnels only.
purchase_selectedactionnativeBuying happens in the app store, so this is available on native funnels only.
purchaseButtonblocknativeBuying happens in the app store, so this is available on native funnels only.
restore_purchasesactionnativeBuying happens in the app store, so this is available on native funnels only.
set_attributeactionnativeSubscriber attributes exist once someone is in the app, so this is available on native funnels only.
stickyFooterblocknativeBuying happens in the app store, so this is available on native funnels only.

Same on both targets

Anything absent from the table above exists on both targets — that default is deliberate, so a new block type is available everywhere unless the crate says otherwise.

The editor reads the same table the publish judge applies: the palette and the action menu disable what the funnel's target cannot carry, with the crate's own sentence beside it (apps/dashboard/src/components/funnel-builder/target-capabilities.tsx:12-18). A forbidden item is shown disabled, never hidden — hiding it would teach the author nothing. While the table is still loading, every capability shows the pending reason instead (apps/dashboard/src/components/funnel-builder/target-capabilities.tsx:47). And the judge refuses with the same rule: BLOCK_NOT_ON_TARGET and ACTION_NOT_ON_TARGET (packages/funnel-core-rs/src/validate.rs:40-41) — so the editor can never offer what publish refuses. See Publishing for the full finding tables.

Changing the target later does not rewrite the funnel — it judges what the other target cannot carry first. See Conversion.

On this page