Aurabase Logo
aurabasedocs
docs›Guides›Client options and errors

Client options, errors, retries and timeouts

The behaviour shared by every service of the SDK, and the options that change it.

9 min read·Level intermediate·Revised on Oct 12, 2026
This English text was generated automatically from the French original and has not been reviewed yet.
#
Create

1. Create the client

src/lib/aurabase.tstypescript
import { createClient } from '@aurabase/aurabase-js'

export const aura = createClient(
  process.env.NEXT_PUBLIC_AURABASE_URL!,       // https://my-app.aurabase.cloud
  process.env.NEXT_PUBLIC_AURABASE_ANON_KEY!,  // the public key
)

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:

server/admin.tstypescript
import { createAdminClient } from '@aurabase/aurabase-js'

// Server only: bypasses row-level security.
export const admin = createAdminClient(process.env.NEXT_PUBLIC_AURABASE_URL!, process.env.AURABASE_SERVICE_ROLE_KEY!)

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.from('table'). See the client reference.

#
Options

2. Options

OptionDefaultEffect
auth.persistSessiontrue in a browserKeep the session between page loads
auth.storagelocalStorage in a browser, memory elsewhereWhere the session lives: pass your own store (cookies for SSR)
auth.autoRefreshtrueRefresh the access token before it expires, and keep tabs in sync
auth.detectSessionInUrltrue in a browserRead a session from the URL after an OAuth redirect, then clean the URL
fetchglobalThis.fetchYour own fetch, for tests or proxies
retrytrueRetry reads on transient errors. false disables, or { enabled, maxRetries }
requestTimeoutMs30000Time limit of a request. 0 disables it.
onEventnoneReceives observability events, see below
debugfalseAlso print each event with console.debug('[aurabase]', event)
refuseServiceRoleInBrowsertrueThrow 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.

#
Errors

3. The error object

Every call returns { data, error } and never throws. When it fails, error is an AuraError:

errors.tstypescript
import { isAuraError } from '@aurabase/aurabase-js'

const { data, error } = await aura.from('posts').select('*')
if (error) {
  console.log(error.status)  // 500, or 0 when the failure is on the client
  console.log(error.code)    // 'INTERNAL_ERROR', 'TIMEOUT', 'NETWORK_ERROR', …
  console.log(error.message) // human-readable
  console.log(error.details) // the raw body, including error.request_id from the server
  isAuraError(error)         // true
}

A real failing call returned:

error.detailsjson
{ "error": { "code": "INTERNAL", "message": "boom", "request_id": "req-123" } }
FieldMeaning
statusThe HTTP status. 0 means the failure happened before any response: network, timeout, or a refusal by the client.
codeA stable identifier. Server codes keep their value; the client adds the codes below.
messageA readable explanation
detailsThe raw response body
retryAfterSeconds to wait, from a Retry-After header, on a 429
isNotFound, isUnauthorized, isForbidden, isRateLimitedShortcuts on status

All error codes are in SCREAMING_SNAKE (^[A-Z][A-Z0-9_]+$), whether the server or the client produced them (error.legacyCode returns the former lowercase form and is deprecated). 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_LIMITED. A 429 always carries RATE_LIMITED, and error.retryAfter holds the Retry-After delay in seconds when the server sent one.

Test data rather than error when you need the value. Some calls succeed without content (204), and then both data and error are null.

#
Resilience

4. Retries and timeouts

Retries. The client retries a request automatically only when all of these hold:

  • the method is GET or HEAD (a POST, PUT, PATCH or DELETE is never replayed, so nothing runs twice);
  • the response status is 408, 429, 502, 503 or 504, 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:

text
GET  /flaky   590 ms  { data: [...] }          events: retry#1:503, retry#2:503
POST /flaky     5 ms  { error: SERVICE_UNAVAILABLE }   (not retried)
GET  /flaky     1 ms  { error: SERVICE_UNAVAILABLE }   with retry: false

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.

#
Observability

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.

kindFieldsWhen
retryattempt, status?, urlBefore a retry
timeouturlA request hit requestTimeoutMs
refresh-successnoneA session refresh succeeded
refresh-discardedreasonA refresh was dropped because the session changed meanwhile
jwks-fetch-failedbaseUrlThe signing keys could not be fetched (local token checks)
storage-blockedopThe browser refused storage access
observe.tstypescript
const aura = createClient(url, key, {
  onEvent: (e) => metrics.increment(`aurabase.${e.kind}`),
})

URLs in events have their query string and fragment removed, because they can carry secrets (?apikey=, signed links).

#
Sessions

6. Sessions and tokens

  • The client sends apikey on every request, and Authorization: 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 401 triggers one refresh and one replay of the request, then stops.
  • In a browser, refreshes are coordinated across tabs.
  • aura.auth.onAuthStateChange(callback) tells you about SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED and MFA_CHALLENGE. See the auth guide.
Last updated · Oct 12, 2026