Skip to content

Clients and call options

Scope $ to another namespace, key, environment or Customer with $.with, and set timeouts, cancellation and retries per call or per client.

$ acts for the key in STARTUP_BUILDER_API_KEY. $.with(scope) binds the same client to another namespace, key, environment or Customer; options set timeouts, cancellation and retries.

Where it runs

One package runs everywhere code calls a Startup. What changes is where the key comes from.

RuntimeKeyHow
A server or a scriptsk_test_ or sk_live_Read from STARTUP_BUILDER_API_KEY
An edge runtime with no process environmentThe same secret key$.with({ key: env.STARTUP_BUILDER_API_KEY })
A browserpk_test_ or pk_live_React hooks, or $.with with the publishable key
A Startup's own FunctionNonectx.$, acting as the Function's Worker
CIA 15-minute xt_ tokennpx startup-builder login --oidc before the script runs

Keys covers each kind.

Scope a client

$.with takes any of namespace, key, env and for, and returns a client with the same Nouns, Modes and Events. A scope only narrows: a client never reaches a namespace its key cannot.

leaseline.app tracks lease renewals for property managers. Its server acts for the signed-in manager, so each Action records that Customer as for and is checked against what that Customer may do:

TypeScript
import { $ } from 'startup-builder'

// The signed-in manager's Customer, from your own session.
const customer = 'customer_harbor'
const manager = $.with({ for: customer })

const page = await manager.Invoice.list({ limit: 20 })

A script that works on two Startups holds one client per namespace:

TypeScript
const sibling = $.with({ namespace: 'renewalnotes.com', key: process.env.RENEWALNOTES_KEY })

await sibling.Offer.list({ limit: 5 })

env: 'test' pins a client to the test environment whatever its key, which keeps a test suite off live data.

Call options

Every method takes an options object after its input. $.with takes the same options as defaults for every call it makes.

OptionDefaultDoes
idempotencyKeyGenerated per callMakes a repeat return the first Action
timeout30 secondsStops waiting for the response
signalNoneAn AbortSignal that stops waiting when it fires
maxRetries3How often to retry what time fixes
headersNoneExtra headers, such as traceparent for tracing
TypeScript
const controller = new AbortController()

const subscription = await $.Offer.subscribe(
  { offer: 'offer_1', quantity: 1 },
  { idempotencyKey: 'manager-4417-annual', timeout: 10_000, signal: controller.signal },
)

A timeout or an abort stops the wait, not the work. A write that reached the platform keeps its Action. A second call with the same idempotencyKey returns that Action instead of running the Verb again.

Retries

The client retries only what time fixes: rate-limited after Retry-After plus jitter, any 5xx, and a dropped connection. Each retry carries the first call's idempotency key, so a write that landed runs once.

It never retries an error the input or the record caused. Refused, conflict and invalid-input need a changed call, and the problem's fix says what to change. A call over a Limit is not an error at all: its Action waits for an Approval, and a retry returns that same Action.

To turn retries off for one call, pass maxRetries: 0. Errors and testing lists every error and whether to retry it.