Skip to content
Indiana
Dashboard
Docs
Browse docs / Code examples

Code examples

Complete integration files and common recipes. Connect your own authentication and configuration.

Download integration example
SDK 0.0.1Updated

Next.js: identity and customer sessions

Use these files in quickstart order. The host integration seam is your existing getSignedInUser function; it must verify the actual login session.

.env.localShell
# Copy these values from your App → Sandbox → SDK setupGROOVE_BASE_URL=https://your-grow-host.exampleGROOVE_PROJECT_ID=your-project-idGROOVE_SECRET_KEY=your-sandbox-server-key # Public browser configuration; no secret keyNEXT_PUBLIC_GROOVE_BASE_URL=https://your-grow-host.exampleNEXT_PUBLIC_GROOVE_PROJECT_ID=your-project-id

Refresh sessions and sign out

Add this module alongside your grow client. Refresh once only on UNAUTHENTICATED and propagate failures. Call disconnectSession on sign-out or user change to abort the host session fetch and old SDK requests, then call your sign-out endpoint. Write retries retain their original requestId/idempotency key.

lib/groove-session.tsTypeScript
import { GrooveError } from "@tinkerlandai/indiana-sdk";import { grow } from "./groove-client"; // Get a fresh token from your own authenticated session endpointlet generation = 0;let pending: { controller: AbortController; promise: Promise<void> } | undefined; function refreshSession(): Promise<void> {  if (pending) return pending.promise;  const expected = generation;  const controller = new AbortController();  const promise = (async () => {    const response = await fetch("/api/groove-session", {      signal: controller.signal, cache: "no-store", credentials: "same-origin",    });    if (!response.ok) throw new Error("User is not signed in");    const { customerToken } = await response.json();    if (expected !== generation)      throw new GrooveError("IDENTITY_CHANGED", "Identity changed during refresh");    if (typeof customerToken !== "string") throw new Error("Invalid session");    grow.identify(customerToken);  })();  pending = { controller, promise };  const clear = () => { if (pending?.promise === promise) pending = undefined; };  void promise.then(clear, clear);  return promise;} // Refresh once and retry when the token expires (10 minutes by default)export async function withSession<T>(call: () => Promise<T>) {  const expected = generation;  try {    return await call();  } catch (error) {    if (!(error instanceof GrooveError) || error.code !== "UNAUTHENTICATED")      throw error;    if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed");    await refreshSession();    if (expected !== generation) throw new GrooveError("IDENTITY_CHANGED", "Identity changed");    return call();  }} // Discard the token before signing the user out of your appexport function disconnectSession() {  generation++;  pending?.controller.abort();  pending = undefined;  grow.reset(); // Discard the customer token and abort pending requests} export async function signOut() {  disconnectSession();  await fetch("/api/logout", { method: "POST" });}

Create checkout from a published plan

This is a browser display fragment. First implement /api/groove-funnel and the host callbacks from the Funnel integration guide. Publish the current product and configure its Sandbox payment binding, then publish a Funnel checkout node referencing the fixed product version. Persist the original start input and resume the same run after payment return. Retry the original command after a timeout instead of creating another checkout.

lib/show-checkout.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().

Server: consume credits before running a job

Accept a customerId already verified and mapped by your backend. jobId is your stable job ID; retries reuse the same idempotency key. Set the server environment variable GROOVE_GENERATIONS_ENTITLEMENT_ID to the stable generations feature ID, not its key or a product version ID. Run the protected job only when accepted is true. See the credit guide for refunds and long-running reservations.

lib/authorize-generation.tsTypeScript
import { groove } from "./groove-server"; const configuredEntitlementId = process.env.GROOVE_GENERATIONS_ENTITLEMENT_ID;if (!configuredEntitlementId) throw new Error("Missing GROOVE_GENERATIONS_ENTITLEMENT_ID"); export async function authorizeGeneration(customerId: string, jobId: string) {  const { receipt } = await groove.mutateBalance({    customerId,    command: {      operation: "consume", entitlementId: configuredEntitlementId,      quantity: "1", properties: {},      idempotencyKey: `job:${jobId}:consume`,    },  });  if (!receipt.accepted) throw new Error(receipt.reason ?? "Credits unavailable");  return receipt;}

Browser: display a published Funnel

Publish the Funnel and its entry configuration. Start on the host backend and display with the run credential. reportError handles failures; destroy and discard old runs on navigation or identity changes.

Related:See the editor and customer preview

show-upgrade.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().