Skip to content
Indiana
Dashboard
Docs
Browse docs / Apps, environments & identity

Apps, environments & identity

Understand organizations, Apps, customers and credential boundaries.

SDK 0.0.1Updated

A scoped identity

  • An organization contains product Apps. Every tenant-owned object belongs to an App and, where applicable, an environment.
  • Sandbox and Live hold separate customers, catalog data, surfaces and events. A credential is permanently bound to its App and environment.
  • Console members and SDK customers are separate identities. Email does not merge customer records.
X-Groove-Project and X-Groove-Environment validate a credential’s scope. They do not grant access or switch Apps.

Sessions and sign-out

  • Tokens stay in SDK memory. Refresh an expired token through your own authenticated backend; the SDK does not refresh automatically.
  • On sign-out or identity change, destroy active surfaces and call disconnectSession to abort the host session fetch as well as resetting the client. Retain requestId/idempotency keys for write retries.
  • The host backend can revoke one session or all customer sessions. Revoking the issuing API Key invalidates its customer tokens on their next request.

Related:Server-key verification and rotation

app/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" });}