Skip to content

JavaScript client

@potalab/base is a small ESM-only TypeScript client with no runtime dependencies. It runs in browsers, Node 22+, Deno and Bun.

import { createClient } from "@potalab/base"
const base = createClient<Database>({ url, key })
createClient<Database>({
url, key, // required: project URL and API key
projectRef, // optional project slug, used to name the storage key
auth: {
storage, // AuthStorage for the refresh token (default: in-memory)
persistSession, // false: always in-memory (default true)
autoRefresh, // refresh ~60 s before expiry (default true)
cookieMode, // refresh token in an HttpOnly cookie (default false)
storageKey,
},
fetch, // custom fetch
headers, // extra headers for every request
db: { schema, maxEmbedDepth }, // schema profile; embed depth limit (default 3, false = off)
cache: { staleTime, gcTime }, // opt-in client cache
retry: { maxRetries: 2, maxDelayMs: 30_000, baseDelayMs: 1_000 },
realtime: { WebSocket, heartbeatIntervalMs, timeoutMs, reconnectMinMs, reconnectMaxMs },
accessToken: async () => token, // externally issued access token
})

Every request carries your key as apikey and, when a user is signed in, Authorization: Bearer <access_token>. A secret key (lb_sec_...) is exchanged for a short-lived service_role token and is refused in browsers. See API keys.

const { data, error, count, status } = await base.from("t").select("*").eq("id", 1).single()
await base.from("t").insert({ ... }).select()
await base.from("t").update({ ... }).eq("id", 1)
await base.from("t").upsert({ ... }, { onConflict: "id" })
await base.from("t").delete().eq("id", 1)
await base.rpc("fn", { arg: 1 })

Details in Querying with the SDK. Results are always { data, error, count, status, statusText }; errors are returned as BaseError, never thrown.

Method Purpose
signUp, signInWithPassword (alias signIn) email and password
signInWithOtp, verifyOtp, signInWithMagicLink passwordless
signInWithOAuth, signInWithSSO, exchangeCodeForSession social and enterprise sign-in
signInAnonymously, linkIdentity guests and account linking
acceptInvite complete an emailed invitation
getSession, getUser, refreshSession, setSession session access
resetPasswordForEmail, signOut recovery and sign-out
onAuthStateChange, startAutoRefresh, stopAutoRefresh lifecycle
mfa.* MFA
tenants.* multi-tenancy
admin.* admin API, secret key only
  • base.storage.from(bucket): upload, download, createSignedUrl, getPublicUrl, list, remove. Bucket admin (createBucket, updateBucket, getBucket, listBuckets, emptyBucket, deleteBucket) needs a secret key. See Storage.
  • base.channel(name, options), base.removeChannel(channel), base.removeAllChannels(), base.getChannels(). Channels support on, subscribe, send, track, untrack, presenceState and unsubscribe. See Realtime.

Error codes you will meet often: invalid_credentials, bad_jwt, over_request_rate_limit, rate_limited, embed_depth_exceeded, network_error. The SDK handles bad_jwt (one refresh and one retry), signs the user out locally on refresh_token_reused, refresh_token_not_found, session_revoked and user_not_found, and retries idempotent requests up to twice on 429. Writes are never retried automatically.

@potalab/base/react provides useQuery(builder, options) and useMutation(fn, options) on top of the client cache:

import { useQuery } from "@potalab/base/react"
function Todos() {
const todos = useQuery(base.from("todos").select("id,title").eq("done", false).order("id"))
if (todos.isPending) return <Spinner />
if (todos.isError) return <p>{todos.error.message}</p>
return todos.data.map((t) => <li key={t.id}>{t.title}</li>)
}

Options include enabled, staleTime, gcTime, keepPreviousData and refetchInterval. Identical queries share one request, writes invalidate dependent queries, and the cache clears on sign-out.