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.
| Runtime | Key | How |
|---|---|---|
| A server or a script | sk_test_ or sk_live_ | Read from STARTUP_BUILDER_API_KEY |
| An edge runtime with no process environment | The same secret key | $.with({ key: |
| A browser | pk_test_ or pk_live_ | React hooks, or $.with with the publishable key |
| A Startup's own Function | None | ctx.$, acting as the Function's Worker |
| CI | A 15-minute xt_ token | npx 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:
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:
const sibling = $.with({ namespace: 'renewalnotes.com', key: process.env.RENEWALNOTES_KEY })
await sibling.Offer.list({ limit: 5 })env: 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.
| Option | Default | Does |
|---|---|---|
idempotencyKey | Generated per call | Makes a repeat return the first Action |
timeout | 30 seconds | Stops waiting for the response |
signal | None | An AbortSignal that stops waiting when it fires |
maxRetries | 3 | How often to retry what time fixes |
headers | None | Extra headers, such as traceparent for tracing |
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:. Errors and testing lists every error and whether to retry it.