Client options, errors, retries and timeouts
The behaviour shared by every service of the SDK, and the options that change it.
1. Create the client
createClient(url, key, options?) takes the project URL and one key. The project is identified by the host of the URL, so there is no project id to pass. In a local stack, use a URL with the project subdomain, such as http://my-app-ab12.localhost:8080; with a bare http://localhost:8080 the client prints a warning, because the gateway cannot tell which project you mean.
Use the public key (aura_anon_…) in browsers and apps. A service_role key (aura_sk_…) belongs on your server only, and the client enforces it:
In a browser, createClient throws when it receives a service_role key, so a leaked key cannot ship by accident. Set refuseServiceRoleInBrowser: false to downgrade the error to a warning; do not do this in production.
Every service hangs off the client: aura.auth, aura.db, aura.storage, aura.realtime, aura.functions, aura.ai, aura.notifications, and aura.from('table') as a shortcut for aura.db.from('table'). See the client reference.
2. Options
| Option | Default | Effect |
|---|---|---|
auth.persistSession | true in a browser | Keep the session between page loads |
auth.storage | localStorage in a browser, memory elsewhere | Where the session lives: pass your own store (cookies for SSR) |
auth.autoRefresh | true | Refresh the access token before it expires, and keep tabs in sync |
auth.detectSessionInUrl | true in a browser | Read a session from the URL after an OAuth redirect, then clean the URL |
fetch | globalThis.fetch | Your own fetch, for tests or proxies |
retry | true | Retry reads on transient errors. false disables, or { enabled, maxRetries } |
requestTimeoutMs | 30000 | Time limit of a request. 0 disables it. |
onEvent | none | Receives observability events, see below |
debug | false | Also print each event with console.debug('[aurabase]', event) |
refuseServiceRoleInBrowser | true | Throw when a service_role key reaches a browser |
For server-side rendering, use the cookie-based clients described in Next.js App Router and SSR.
3. The error object
Every call returns { data, error } and never throws. When it fails, error is an AuraError:
A real failing call returned:
| Field | Meaning |
|---|---|
status | The HTTP status. 0 means the failure happened before any response: network, timeout, or a refusal by the client. |
code | A stable identifier. Server codes keep their value; the client adds the codes below. |
message | A readable explanation |
details | The raw response body |
retryAfter | Seconds to wait, from a Retry-After header, on a 429 |
isNotFound, isUnauthorized, isForbidden, isRateLimited | Shortcuts on status |
Codes produced by the client itself (status 0): timeout, network_error, invalid_response (a success whose body is not JSON), no_session, refresh_discarded, invalid_path, invalid_body. Authentication adds codes such as invalid_credentials, weak_password, email_already_in_use, user_not_found, invalid_otp, session_expired, session_revoked and rate_limit_exceeded. A 429 always carries rate_limit_exceeded.
Test data rather than error when you need the value. Some calls succeed without content (204), and then both data and error are null.
4. Retries and timeouts
Retries. The client retries a request automatically only when all of these hold:
- the method is
GETorHEAD(aPOST,PUT,PATCHorDELETEis never replayed, so nothing runs twice); - the response status is
408,429,502,503or504, or the network failed; - fewer than 3 retries have happened.
The delay is random between 0 and min(500 ms × 2^attempt, 10 s). A Retry-After header wins over it, capped at 60 seconds. Retried requests carry an X-Retry-Count header. A timeout is not retried.
Measured against a server that answered 503 twice, then 200:
Timeouts. A request that gets no answer within requestTimeoutMs (30 s by default) ends with status: 0 and code: 'timeout'. Reading the body is bounded by the same limit, so a server that sends headers and then stalls cannot hang the call. A request to a server that never answered, with a 400 ms limit, returned the error after 405 ms.
Two cases do not use this limit because they manage their own: a call where you pass an AbortSignal, and uploads. Functions use their own timeout option, see Errors, timeouts and retries.
5. Observe the client
onEvent receives one event per notable thing the client does. It never makes a request fail: an exception thrown by your hook is ignored.
kind | Fields | When |
|---|---|---|
retry | attempt, status?, url | Before a retry |
timeout | url | A request hit requestTimeoutMs |
refresh-success | none | A session refresh succeeded |
refresh-discarded | reason | A refresh was dropped because the session changed meanwhile |
jwks-fetch-failed | baseUrl | The signing keys could not be fetched (local token checks) |
storage-blocked | op | The browser refused storage access |
URLs in events have their query string and fragment removed, because they can carry secrets (?apikey=, signed links).
6. Sessions and tokens
- The client sends
apikeyon every request, andAuthorization: Bearer <access token>once a user is signed in. - It refreshes the access token when fewer than 30 seconds remain. Concurrent refreshes are merged into one, so a rotating refresh token is used once.
- A
401triggers one refresh and one replay of the request, then stops. - In a browser, refreshes are coordinated across tabs.
aura.auth.onAuthStateChange(callback)tells you aboutSIGNED_IN,SIGNED_OUT,TOKEN_REFRESHED,USER_UPDATEDandMFA_CHALLENGE. See the auth guide.