Skip to content
Indiana
Dashboard
Docs
Browse docs / Funnel screens & Paywalls

Funnel screens & Paywalls

Publish screens and use one runtime protocol for display and business actions.

SDK 0.0.1Updated

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.

Related:Backend receipts, tasks and recovery

app/api/groove-funnel/route.tsTypeScript
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.
app/groove-funnel.tsTypeScript
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().