Rovenue SDK
FunnelsNative

The Native Path

How a placement serves a native funnel to the device — the envelope, the variant draw, and what the device caches.

A native funnel has no URL. It reaches the device one way: a placement resolves to it, and the app asks for that placement by identifier. The published document rides inside the placement envelope — the device never fetches a funnel by its own id.

A placement row targets a native funnel

In the dashboard a placement is an ordered list of audience rows, walked top-down; the first matching row wins. A row may name a native funnel, or a funnel experiment whose variants each name one (apps/api/src/lib/placement-resolution.ts:126-137). The served shape is the published document, not the draft (apps/api/src/lib/placement-resolution.ts:132-135) — which is what makes revision meaningful, because a device comparing a stored session against it compares against something immutable.

What the endpoint serves

GET /v1/placements/:identifier (apps/api/src/routes/v1/placements.ts:34) resolves the placement for the calling subscriber and returns the envelope (apps/api/src/lib/placement-resolution.ts:163-191): placement, the resolved funnel under its own key (apps/api/src/lib/placement-resolution.ts:167), and experiment beside it — a placement that draws a funnel-experiment variant serves it under funnel, exactly as a direct funnel target does.

An unknown or retired placement is the empty envelope with 200 — never 404 (apps/api/src/routes/v1/placements.ts:44-54). A shipped app must never crash because a placement changed server-side, so getFunnel resolves to nothing and the funnel view stays empty.

{
  "data": {
    "placement": { "identifier": "onboarding", "revision": 7 },
    "funnel": {
      "identifier": "summer-quiz",
      "id": "<the funnels row id>",
      "revision": 3,
      "configFormatVersion": 3,
      "config": { "...": "the published document" },
      "offering": { "...": "hydrated, or null when it sells nothing" }
    },
    "experiment": null
  }
}

funnel.id is the funnels row id — the device stamps it into its funnelContext, because the slug can be null for native funnels (apps/api/src/lib/placement-resolution.ts:139-143). offering is hydrated with the offering's packages, and is null for a funnel that sells nothing (apps/api/src/lib/placement-resolution.ts:145-158).

The variant draw is client-side and deterministic

The server materializes every variant into a fully resolved funnel and sends the undrawn menu; the device draws. The bucket is sha256("{subscriber_id}:{seed}"), first 4 bytes big-endian, mod 10,000 — the cross-language contract, byte for byte the same in Rust and TypeScript (packages/core-rs/src/placements/bucketing.rs:5-14). The variant is picked by walking the weights in order against that bucket (packages/core-rs/src/placements/bucketing.rs:18-27), so the same subscriber plus the same experiment key draws the same variant on every device — including offline, from cache.

What the device caches

The device persists the last-known placement response per placement — the raw envelope JSON keyed placement:{identifier} — and re-resolves it when the network is unavailable, bucket draw and variant selection included (packages/core-rs/src/cache/placements.rs:7-12). A cached envelope predates anything created after it was frozen: an experiment started later does not exist in it, and the device only sees the experiment once it re-resolves live. For first-launch-with-no-cache coverage, bundle the Offline fallback file.

servedFromFallback on the resolved funnel tells you which path answered: true only when the bundled fallback file did — network and disk cache both missed (packages/core-rs/src/librovenue.udl:168).

Next: Show a funnel.

On this page