Design screens independently and connect button routes, conditions, authentication, purchases and completion nodes in the flow canvas. Back navigation follows the visited path. Default selections are not submitted answers.
Onboarding & Funnels
Connect independent screens, branches, authentication and purchase nodes.
Design a flow
Start and render
Your backend calls createFunnelStartClient().start() with a customer session: the signed-in customer's session, or for visitors a guest session from createGuestCustomer then resumeGuestSession. runToken authorizes browser flow operations until credentialExpiresAt; the separate runExpiresAt limits business execution. After the guest is claimed at login, the run continues through link with its assignment unchanged.
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().Resume and measure
- A run pins the published version and assignment. Authentication preserves that assignment.
- The server stores the current node; your host persists local answers for the same version. destroy only removes UI; close also submits a close event.
- onAuthenticate obtains the current signed-in user’s customer token from your backend.
- Experiment reports use actual reached exposures. Client-side completion does not establish revenue.
Start on the host backend
This page describes the current source protocol. Match the SDK, Indiana deployment and database migrations. npm 0.0.1 beta is published; this does not establish publication of later workspace versions. Hosts own verified sessions and persist start input. Close and discard old runs when changing accounts.
import { createFunnelStartClient, createGrooveServer, createGuestToken,} from "@tinkerlandai/indiana-sdk/server"; const options = { baseUrl: process.env.GROOVE_BASE_URL!, apiKey: process.env.GROOVE_SECRET_KEY!, scope: { projectId: process.env.GROOVE_PROJECT_ID!, environment: "sandbox" },} as const;const groove = createGrooveServer(options);const funnels = createFunnelStartClient(options); export async function POST(request: Request) { if (request.headers.get("origin") !== process.env.APP_ORIGIN) return new Response("Forbidden", { status: 403 }); const session = await requireHostSession(request); // Every run has a customer: the signed-in customer, or a guest kept in this session. let customerToken: string; if (session.grooveCustomerId) { ({ customerToken } = await groove.createCustomerSession(session.grooveCustomerId)); } else { session.grooveGuestToken ??= createGuestToken(); await session.save(); await groove.createGuestCustomer({ guestToken: session.grooveGuestToken }); ({ customerToken } = await groove.resumeGuestSession({ guestToken: session.grooveGuestToken })); } // Persist before the first request. Retries reuse the whole original input. if (!session.funnelStart) { session.funnelStart = { source: { type: "funnel", id: process.env.GROOVE_FUNNEL_ID! }, requestId: crypto.randomUUID(), locale: session.locale, commerce: { provider: "stripe", currency: "USD" }, }; await session.save(); } const result = await funnels.start({ ...session.funnelStart, customerToken }); session.grooveRunToken = result.runToken; await session.save(); return Response.json({ runToken: result.runToken }, { headers: { "Cache-Control": "private, no-store" }, });}Display, resume and customer components
obtainCurrentUserCustomerToken first completes host sign-in, then obtains the current user’s token from your backend; cancellation returns null. requireHostSession, session.save and reportError are explicit host integration points. Retain the original start input and remount the same run after returning from checkout.
- Account, membership, credits and billing are Funnel screen components. They read the current authenticated customer; purchase and membership buttons must connect to explicit node outputs in the editor.
- A Paywall can start with source: { type: 'paywall', id }; the server uses its system-managed Funnel. Separate Page and remote Popup endpoints are unnecessary.
- destroy only removes UI; close submits a closure command. Browser caching retains design assets only; customer data and run state remain private.
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://grow.your-domain.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});const handle = await showFunnelRun(container, { client, onAuthenticate: async () => obtainCurrentUserCustomerToken(), onCheckout: ({ url }) => window.location.assign(url), onOpenUrl: ({ url }) => window.location.assign(url), onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().