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 })Options
Section titled “Options”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 |
Storage and realtime
Section titled “Storage and realtime”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 supporton,subscribe,send,track,untrack,presenceStateandunsubscribe. See Realtime.
Error handling and retries
Section titled “Error handling and retries”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.
React hooks
Section titled “React hooks”@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.