Create server keys and understand per-request verification, scope, permissions, revocation and rotation.
SDK 0.0.1Updated
Start with three identities
Ordinary server SDK calls use a gg_sk_ API key. The key is bound to one App/environment, and Indiana checks its permissions on every request. Server reads and writes do not require issuing a customer token first.
Who holds each credential
Caller
Credential
Purpose
Your backend
GROOVE_SECRET_KEY / gg_sk_
Call Indiana within the App/environment and permissions
Your client
customerToken / gg_ct_
Call allowed customer endpoints directly; issued by your backend
Indiana console member
Supabase Auth
Sign in to the console and create Apps and keys
Who holds each credential
Your backend
Credential
GROOVE_SECRET_KEY / gg_sk_
Purpose
Call Indiana within the App/environment and permissions
Your client
Credential
customerToken / gg_ct_
Purpose
Call allowed customer endpoints directly; issued by your backend
Indiana console member
Credential
Supabase Auth
Purpose
Sign in to the console and create Apps and keys
A Customer is an Indiana business record mapped to your product's user by App, environment and externalUserId. It does not share console login credentials. Your authentication system verifies end users; an Indiana key does not prove user login. Complete the server-key-only flow below before considering client sessions.
1. Create a Sandbox server key
Sign in as an Owner, Admin or Developer with App access and apiKeys:write, then select your App and Sandbox in SDK setup.
Create a key with customers:write, the only permission required for the first request below. Plaintext is shown once; save it in your backend configuration.
Copy the base URL and App ID from the same page. Use the site root as base URL; do not append /api/v1. Server-only requests do not require a browser origin.
A key is bound to one App/environment and expires in 1–365 days. Create a replacement to change permissions or expiry. Each App/environment supports up to 20 active keys.
2. Install, configure and make your first request
Use Node.js 22 or later. This guide pins the published npm 0.0.1 beta; install it directly.
Save a Git-ignored .env.local in your app root and replace the App ID and key below. For self-hosted Indiana, use your own site root. Read GROOVE_SECRET_KEY on the backend only; never prefix it with NEXT_PUBLIC_.
Create groove-server-check.mjs in the same directory and copy the complete script below. It creates and retains one Sandbox test customer. It does not issue a customer token. Customer creation can assign an eligible configured default plan or automatic trial.
First request complete
requestId, Sandbox scope and customerId confirm that Indiana verified the key and completed the database operation. Rerunning returns the same customerId with created: false, which is also success. Constructing the SDK makes no authentication request; verification occurs when an API is called.
In your application, replace the fixed test externalUserId with a stable user ID verified by your backend or authorized for your business task. A browser-supplied customerId or user ID is not authorization.
TerminalShell
1npm install @tinkerlandai/indiana-sdk@0.0.1
.env.localShell
1# Copy Sandbox values from SDK setup; ignore this file in Git.2GROOVE_BASE_URL=https://groove-grow.vercel.app3GROOVE_PROJECT_ID=your-app-id4GROOVE_SECRET_KEY=your-sandbox-server-key
groove-server-check.mjsJavaScript
1import { createGrooveServer, GrooveError } from"@tinkerlandai/indiana-sdk/server";23classConfigurationErrorextendsError {}45functionrequired(name) {6const value = process.env[name];7if (!value) thrownewConfigurationError("Missing environment variable: " + name);8return value;9}1011try {12const groove = createGrooveServer({13 baseUrl: required("GROOVE_BASE_URL"),14 apiKey: required("GROOVE_SECRET_KEY"),15 scope: {16 projectId: required("GROOVE_PROJECT_ID"),17 environment: "sandbox",18 },19 });2021// A fixed Sandbox test identity; reruns return the same Customer.22const { requestId, scope, customer, created } =23await groove.getOrCreateCustomer({24 externalUserId: "docs-server-auth-check",25 });2627 console.log(JSON.stringify({28 requestId, scope, customerId: customer.id, created,29 }, null, 2));30} catch (error) {31if (error instanceof GrooveError) {32// Log diagnostic fields only, never keys, tokens or the full exception.33 console.error(JSON.stringify({34 code: error.code, status: error.status, requestId: error.requestId,35 }));36 } elseif (error instanceof ConfigurationError) {37 console.error(error.message);38 } else {39 console.error("The request did not complete. Check Node, the environment file and network configuration.");40 }41 process.exitCode = 1;42}
HTTP uses the same key and permission as the SDK. Set GROOVE_BASE_URL, GROOVE_PROJECT_ID and GROOVE_SECRET_KEY in your terminal environment before this command; Node's --env-file does not export variables to your shell.
Success returns HTTP 200 with requestId, scope, created and customer. Repeating the same App, environment and externalUserId returns the existing customer.
SDK scope must match the key's App/environment. X-Groove-Project and X-Groove-Environment assert the expected scope; changing them cannot authorize another App. Direct HTTP may omit them, but must send both when used.
Refunds additionally require credits:manage alongside credits:consume. Funnel start requires sessions:create and funnels:run. Check each endpoint for additional conditions.
If your application only calls Indiana from its backend, the flow above is sufficient. Add customer sessions when the browser needs current-customer data or Funnel presentation.
Your backend verifies login through your authentication system, then maps the user's Customer.
Add sessions:create to the key plus the client's required permissions, such as entitlements:read.
Your backend calls createCustomerSession(customer.id), returns customerToken/expiresAt only to the verified user, and sets no-store.
After identify(customerToken), the client can call Indiana directly. Add the browser origin to the current App/environment allowlist.
Customer tokens normally last 10 minutes, capped by key expiry. Only entitlements:read, funnels:run, billing:read, billing:write, offers:resolve and credits:read are delegated. Every request rechecks the issuing key and permissions. Guest sessions receive funnels:run only.
There is no Public SDK Key plus arbitrary user-ID bootstrap. Ordinary SDK handlers reject server-key requests carrying Origin; keep secret keys out of browsers. See the Funnel reference for its separate gateway's Origin rules.
Check that the full key was copied and loaded by your backend and is not revoked or expired. Refreshing a customer token cannot fix a server-key failure.
403 / FORBIDDEN
The first request requires customers:write. Ordinary server requests must not carry a browser Origin.
403 / SCOPE_MISMATCH
Check the App ID and sandbox/live against the key's scope.
400 / INVALID_REQUEST
Check the DTO, paired scope headers and JSON body.
500 / INTERNAL_ERROR; 503 / SERVICE_UNAVAILABLE
Record code, status and requestId to investigate dependencies/network failures. Preserve the original identity and input for write retries.
501 / NOT_CONFIGURED
A required Indiana service is unconfigured; its operator must resolve it.
Common failures and next steps
401 / UNAUTHENTICATED
Check first
Check that the full key was copied and loaded by your backend and is not revoked or expired. Refreshing a customer token cannot fix a server-key failure.
403 / FORBIDDEN
Check first
The first request requires customers:write. Ordinary server requests must not carry a browser Origin.
403 / SCOPE_MISMATCH
Check first
Check the App ID and sandbox/live against the key's scope.
400 / INVALID_REQUEST
Check first
Check the DTO, paired scope headers and JSON body.
500 / INTERNAL_ERROR; 503 / SERVICE_UNAVAILABLE
Check first
Record code, status and requestId to investigate dependencies/network failures. Preserve the original identity and input for write retries.
501 / NOT_CONFIGURED
Check first
A required Indiana service is unconfigured; its operator must resolve it.
HTTP errors use { requestId, error: { code, message } }. SDK GrooveError carries code, status and requestId; the SDK does not retry automatically. The script logs these diagnostic fields only, never keys or tokens.
Server-only rotation: create a replacement key for the same App/environment with required permissions, update backend configuration, confirm requests, then revoke the old key.
For client integration, also issue customer sessions with the new key and update clients before revoking the old key. renewCustomerSession requires the same issuer; it cannot renew across keys.
After revocation or expiry, the key and customer tokens it issued fail subsequent authentication. Committed operations are not rolled back.
The SDK does not renew automatically. Reauthenticate through the host before issuing or renewing customer tokens. grow.reset clears memory only; revoke sessions explicitly through the backend's revokeCustomerSessions.
gg_sk_ is an opaque random Bearer credential sent over HTTPS. Plaintext is returned only at creation; the database stores a SHA-256 hash. It is not a Supabase user JWT or a per-request body signature.
Each request parses Bearer, hashes it with SHA-256 and looks it up through grow_authenticate_sdk.
Check revocation, expiry and App/environment records, build a verified principal, then validate endpoint permissions, scope, Origin and input.
Execute business and ownership checks with trusted scope, then validate the response. Successful authentication is not cached across requests; failed verification cannot execute the operation.
Appendix: internal credentials and database access
Integrating apps configure GROOVE_SECRET_KEY only. SUPABASE_SECRET_KEY is Indiana's internal database credential; you do not need it. The SDK does not expose a database connection. Customers live in customers, key hashes in api_keys, and session hashes in customer_sessions.
Ordinary console operations use member Supabase Auth, current membership and RLS, with writes through grow_console_writer. The SDK gateway and scoped business RPCs are separately documented service_role exceptions, relying on trusted scope and ownership/business checks rather than console-member RLS.
The current Redis request-limit policy is observe: record without blocking. A RATE_LIMITED error contract does not establish enforced rate limiting.