Browse documentation

@motherbase/client exposes a Supabase-shaped client over Motherbase HTTP and Socket.IO endpoints. Its public barrel is packages/client/src/index.ts; the package manifest exposes only that root entry.

Create a client

import { createClient, localStorageStorage } from '@motherbase/client'
 
const client = createClient(
  'https://api.example.com',
  'publishable-key',
  {
    storage: localStorageStorage(),
    autoRefresh: true,
    headers: { 'x-client-name': 'web' },
  },
)
 
await client.hydrate()

createClient<Database, SchemaName, Schema>(url, key?, options?) returns a MotherbaseClient. The class constructor has the same arguments. The optional key is sent as the apikey header. Source: packages/client/src/index.ts.

ClientOptions fieldBehavior
storageSessionStorageAdapter; defaults to memoryStorage()
autoRefreshRevalidate once after an authenticated 401; defaults to true
fetchFetch override; otherwise uses a bound globalThis.fetch
headersExtra headers merged into every request
realtimeIoFactoryCustom IoFactory for tests or alternate Socket.IO transports

Construction throws when neither an override nor globalThis.fetch exists. hydrate() loads persisted auth state once and is idempotent. Source: packages/client/src/index.ts and packages/client/src/session.ts.

Runtime exports

ExportPurpose
createClientFactory for MotherbaseClient
MotherbaseClientOwns auth, storage, functions, realtime, query builders, RPC calls, and channels
BackendErrorInternal HTTP failure class also exposed for diagnostics
memoryStorageProcess-local session adapter
localStorageStorageBrowser localStorage adapter with an in-memory fallback
cookieStorageBrowser cookie adapter with an in-memory fallback
serverCookieStorageCookie adapter over batch getAll/setAll methods
PostgrestQueryBuilderChainable and awaitable REST query implementation
AuthClientAuth methods under client.auth
StorageClient / StorageBucketStorage namespace and bucket handle
FunctionsClientEdge-function invoker under client.functions
RealtimeChannelBroadcast, presence, and Postgres-change channel
MfaClient / AuthAdminClientClasses behind client.auth.mfa and client.auth.admin
FunctionsError, FunctionsHttpError, FunctionsRelayError, FunctionsFetchErrorsupabase-js error classes; invoke returns an instance in error rather than throwing
keyValueStorageWraps a supabase-js key/value store as a SessionStorageAdapter
normalizeSession / normalizeUserCoerce a foreign session/user payload into this SDK's shape
SESSION_COOKIE_KEYDefault cookie/storage key, motherbase.session

All are re-exported by packages/client/src/index.ts.

Session storage

memoryStorage() retains one Session | null in memory. localStorageStorage(key = 'motherbase.session') JSON-serializes the session, ignores malformed stored JSON, and falls back to memory when localStorage is absent. Source: packages/client/src/session.ts.

cookieStorage(options?) uses document.cookie. Its options are key, path, secure, and sameSite; defaults are motherbase.session, /, true, and lax. serverCookieStorage(cookies, key?, { secure? }?) serializes through:

interface CookieMethods {
  getAll(): { name: string; value: string }[]
  setAll(cookies: CookieToSet[]): void
}

The server cookie defaults to path: '/', sameSite: 'lax', and secure: true. Clearing it writes maxAge: 0. Source: packages/client/src/cookie.ts.

A custom adapter implements:

interface SessionStorageAdapter {
  get(): Session | null | Promise<Session | null>
  set(session: Session | null): void | Promise<void>
}

Auth API

Every expected auth failure resolves with an error value rather than throwing. Source: packages/client/src/auth.ts.

The table below covers the core session methods. The full surface — signInWithSSO, resend, getSessionFromUrl, getUserIdentities / linkIdentity / unlinkIdentity, signInWithIdToken, getClaims, startAutoRefresh / stopAutoRefresh, and the client.auth.mfa, client.auth.admin, client.auth.organization, client.auth.passkey and client.auth.sso namespaces — is documented with examples in the SDK reference.

MethodInputResult and behavior
signUp(credentials)SignUpCredentials: email, password, optional username{ data: { user, session }, error }; sends username as Better Auth's name, falling back to email
signInWithPassword(credentials)email, passwordPersists and emits the returned bearer session
resetPasswordForEmail(email)Email stringRequests a password-reset OTP; returns { error }
resetPassword(args)email, otp, passwordCompletes the OTP password reset; returns { error }
signInWithOtp(credentials)SignInWithOtpCredentials with emailSends a sign-in OTP; returns { data: {}, error }
verifyOtp(params)email, token, optional type: 'email'Exchanges the OTP for { user, session }
signInWithOAuth(credentials)Provider plus optional redirect optionsReturns { provider, url } and redirects window.location.href unless skipped
updateUser(attributes)Profile data and/or password plus currentPasswordUpdates profile, changes password, then calls getUser()
deleteUser(attributes?)Optional password, token, callbackURLCalls self-service deletion and clears local session on success
setSession({ access_token, refresh_token? })Externally obtained bearer tokenTemporarily adopts, validates, then persists the session; refresh token is unused
signOut()NoneBest-effort server sign-out; always clears local session and returns { error: null }
getUser()NoneFetches the current user from the server
getSession()NoneReads the hydrated local session without a network call
refreshSession()NoneRevalidates the Better Auth session; keeps the access token, refreshes the user, or clears expired state
onAuthStateChange(callback)AuthChangeListenerReturns { data: { subscription } }; subscription.unsubscribe() removes the listener

UpdateUserAttributes.password requires currentPassword; the client returns a local error before sending a blind password change. OAuth options are redirectTo, space-separated scopes, and skipBrowserRedirect. Events are SIGNED_IN, SIGNED_OUT, and TOKEN_REFRESHED.

Organizations, teams and runtime SSO

The SDK includes thin, typed wrappers around the Better Auth organization, passkey and SSO plugin endpoints. They use the same bearer session and { data, error } result envelope as the rest of the client; expected HTTP failures do not throw.

const { data: organization } = await client.auth.organization.create({
  name: 'Acme',
  slug: 'acme',
})
 
await client.auth.organization.inviteMember({
  organizationId: organization!.id,
  email: 'person@acme.example',
  role: 'member',
})
 
await client.auth.organization.acceptInvitation({ invitationId })

client.auth.organization covers organization lifecycle, active organization, members, invitations, and teams. Calls with an optional organizationId use the session's active organization when it is omitted. Teams are only available when auth.organization.teamsEnabled is enabled. The server exposes only the built-in owner, admin and member roles; dynamic roles are not enabled.

client.auth.sso manages runtime identity-provider records. Registering an organization provider requires auth.sso.enabled and organization owner/admin permissions:

await client.auth.sso.register({
  providerId: 'acme-okta',
  issuer: 'https://acme.okta.com',
  domain: 'acme.example',
  organizationId: organization!.id,
  oidcConfig: {
    clientId: '…',
    clientSecret: '…',
    discoveryEndpoint: 'https://acme.okta.com/.well-known/openid-configuration',
  },
})

The provider namespace also has listProviders, getProvider, updateProvider and deleteProvider. Provider secrets are withheld from list/get responses. SSO sign-in itself is client.auth.signInWithSSO({ email, providerId, organizationSlug, or domain, options: { redirectTo } }).

Better Auth supplies one session token. Tokens.refreshToken and setSession().refresh_token exist only for source compatibility and are not used.

HTTP and error handling

HttpClient in packages/client/src/http.ts hydrates storage before every request, merges client/request headers, attaches Authorization: Bearer <accessToken> unless a request is anonymous, and handles JSON, multipart, raw JSON, or raw Response bodies.

On an authenticated 401, concurrent calls share one refresh promise. A successful revalidation retries the original request once; a failed revalidation clears auth state. The public modules catch BackendError and convert it with toError:

interface MotherbaseResponse<T> {
  data: T | null
  error: MotherbaseError | null
}
 
interface MotherbaseError {
  message: string
  status?: number
  code?: string
}

BackendError exposes httpStatus, optional API apiStatus, and optional backend code. Programmer misuse can still throw, including a missing fetch implementation, an invalid is() value, an unsupported channel event type, or missing realtime transport.

Typed database client

Pass generated database types to the factory. The type plumbing is in packages/client/src/database.ts.

import { createClient } from '@motherbase/client'
import type { Database } from './database.types'
 
const client = createClient<Database>('https://api.example.com', 'publishable-key')
 
const { data, error, count } = await client
  .from('products')
  .select('id,name,price', { count: 'exact' })
  .eq('active', true)
  .lt('price', 50)
  .order('name')
  .range(0, 9)

With a generated Database, relation names, selected columns, row shapes, inserts, updates, filter columns, and RPCs are checked. Views are selectable but their write types resolve to never. A select containing relational embedding syntax falls through the EmbeddedSelectSeam because the server does not implement typed relation resolution. Without a database generic, GenericDatabase accepts arbitrary relations and record-shaped rows.

Query verbs

MethodBehavior
select(columns = '*', options?)GET/HEAD selection; count is exact, planned, or estimated and head requests count only
insert(values, options?)POST one row or an array; defaultToNull: false sends missing=default
upsert(values, options?)POST with merge/ignore duplicate resolution; supports onConflict
update(values, options?)PATCH filtered rows
delete(options?)DELETE filtered rows
rpc(fn, args?, options?)POST by default; get or head moves arguments to query parameters; supports count

Calling select() after a mutation requests return=representation. Bulk inserts collect the union of object keys into a columns query parameter. RpcOptions is { head?: boolean; get?: boolean; count?: PostgrestCount }. Source: packages/client/src/index.ts and packages/client/src/postgrest.ts.

Filters and modifiers

FamilyMethods
Comparisoneq, neq, gt, gte, lt, lte
Patternlike, ilike, likeAllOf, likeAnyOf, ilikeAllOf, ilikeAnyOf
Set/nullin, is, match
Escape hatchesnot, filter, or
Full-texttextSearch with plain, phrase, or websearch
Array/range/JSONcontains, containedBy, overlaps
Range positionrangeGt, rangeGte, rangeLt, rangeLte, rangeAdjacent
Result modifiersorder, limit, range, abortSignal, returns / overrideTypes, csv, geojson, explain, single, maybeSingle
Write guardsthrowOnError (reject instead of resolving), maxAffected(n) (Prefer: handling=strict, max-affected=n), rollback (Prefer: tx=rollback)
InspectiontoPath, toHeaders

Repeated filters are appended, so two filters on one column are ANDed instead of overwriting each other. range(from, to) is inclusive. order, limit, and range accept referencedTable or the legacy foreignTable name.

single() returns error code PGRST116 unless exactly one row is present. maybeSingle() returns the first row or null. csv() resolves to text; explain() requests a text plan or parses JSON when format: 'json'. Count comes from Content-Range and is otherwise null.

Storage

client.storage.from(bucket) returns a StorageBucket from packages/client/src/storage.ts.

const avatars = client.storage.from('avatars')
 
const { data: uploaded, error } = await avatars.upload('users/u1.png', file, {
  visibility: 'public',
  upsert: true,
})
 
if (uploaded) {
  const download = await avatars.download(uploaded.path)
  const signed = await avatars.createSignedUrl(uploaded.path, 3600)
  const publicUrl = avatars.getPublicUrl(uploaded.path)
  const objects = await avatars.list('users/', { limit: 20 })
  await avatars.remove([uploaded.path])
}
MethodActual behavior
upload(path, file, options?)Multipart upload with bucket, path, optional visibility and upsert; returns { path, id, fullPath }
download(pathOrId)Returns a Blob
remove(paths)Deletes all paths concurrently and returns one StorageRemoveResponse per path
list(prefix?, options?)Lists objects with limit, offset, search, and sort query parameters
getPublicUrl(path)Synchronous { data: { publicUrl } } for the public bucket route
createSignedUrl(path, expiresIn?)GET object-url?expiresIn=…; returns { signedUrl, expiresAt }, where expiresAt is null when nothing actually expires
createSignedUrls(paths, expiresIn?)One createSignedUrl per path, reported per entry; no batch endpoint behind it
update(path, file, options?)upload with upsert forced on
copy(from, to, { upsert? }) / move(from, to, { upsert? })POST /api/files/copy and /api/files/move within this bucket
info(path)Object metadata without downloading it
exists(path)true, or false on a 404. Any other failure is an error, not "no"
createSignedUploadUrl(path, { size, contentType?, visibility? })Reserves the upload against the bucket quota and returns { signedUrl, token, path }. S3 driver only; size is required because the signature is bound to that exact length
uploadToSignedUrl(path, token, file, { contentType? })PUTs the bytes, then finalises server-side. The object is only readable after the finalise

A bare UUID is treated as the legacy server file id; other values are treated as a key within the selected bucket. UploadOptions.contentType is declared but the current upload implementation does not forward it.

Realtime channels

socket.io-client is an optional peer imported only by connect() in packages/client/src/realtime.ts.

const channel = client
  .channel('room:products', {
    config: {
      broadcast: { self: true, ack: true },
      presence: { key: 'user-1' },
    },
  })
  .on('postgres_changes', { event: '*', schema: 'public', table: 'products' }, payload => {
    console.log(payload.eventType, payload.new, payload.old)
  })
  .on('broadcast', { event: 'cursor' }, message => {
    console.log(message.payload)
  })
  .on('presence', { event: 'sync' }, () => {
    console.log(channel.presenceState())
  })
  .subscribe(status => console.log(status))

channel(name, options?) reuses the existing channel with the same name. removeChannel(channel) and removeAllChannels() unsubscribe and return 'ok'.

RealtimeChannel supports:

  • on('postgres_changes', filter, callback) for INSERT, UPDATE, DELETE, or *; schema defaults to public.
  • on('broadcast', { event }, callback), with * matching all broadcast events.
  • on('presence', { event: 'sync' | 'join' | 'leave' }, callback).
  • subscribe(callback?, timeoutMs?), returning the channel immediately; the ChannelStatus type is SUBSCRIBED, TIMED_OUT, CLOSED, or CHANNEL_ERROR.
  • send({ type: 'broadcast', event, payload? }), track(state), untrack(), presenceState(), and unsubscribe().

Broadcast acknowledgements and self-delivery are selected in RealtimeChannelOptions.config. Presence state is replaced from the server's full state and diffed locally into join/leave events. Postgres payloads contain schema, table, eventType, new, old, and commit_timestamp. Source: packages/client/src/channel.ts.

The low-level client.realtime exposes connected, getStatus(), onStatusChange(), connect(), disconnect(), on(), off(), emit(), and deprecated watchTable(). watchTable() can poll REST when disconnected; channel subscriptions call connect({ fallbackPolling: false }) and therefore require Socket.IO. Realtime statuses are idle, connecting, connected, reconnecting, polling, disconnected, and error.

Edge functions

FunctionsClient.invoke<Result, Body>(name, options?) is implemented in packages/client/src/functions.ts.

const { data, error } = await client.functions.invoke<{ greeting: string }>(
  'hello',
  { body: { name: 'Ada' } },
)

By default it uses the current session bearer. FunctionInvokeAuth can explicitly select { type: 'public' } or { type: 'edge-token', token }; both suppress the session bearer, and the latter sends x-motherbase-edge-token.

Complete type export index

The following types are re-exported by packages/client/src/index.ts:

AreaTypes
CoreApiEnvelope, User, Tokens, Session, FileObject, FileVisibility, StorageUploadResponse, StorageRemoveResponse, UploadInput, SessionStorageAdapter, ClientOptions, MotherbaseResponse, MotherbaseError, AuthChangeEvent, AuthChangeListener
AuthSignUpCredentials, SignInWithPasswordCredentials, SignInWithOtpCredentials, VerifyOtpParams, SignInWithOAuthCredentials, UpdateUserAttributes, DeleteUserAttributes, Subscription
CookiesCookieMethods, CookieToSet, CookieOptions
RESTRpcOptions, PostgrestSingleBuilder, PostgrestTextBuilder, PostgrestExplainBuilder, PostgrestResponse, PostgrestPrimitive, PostgrestArrayOperand, PostgrestFilterOperator, PostgrestIsValue, PostgrestCount, TextSearchType, ReferencedTableOption, OrderOptions, SelectOptions, InsertOptions, UpsertOptions, MutateOptions, ExplainOptions
Database typingGenericDatabase, GenericSchema, GenericTable, GenericView, GenericFunction, DefaultSchemaName, SchemaOf, Relations, RelationName, RowOf, InsertOf, UpdateOf, FunctionName, FunctionArgs, FunctionReturns, TypedQuery, TypedSingleQuery, SelectResult, ValidateSelect, UnknownSelectColumns, EmbeddedSelectSeam, ColumnName, FilterValue
Storage/functionsUploadOptions, UploadResult, ObjectInfo, ListOptions, FunctionInvokeAuth, FunctionInvokeOptions
Auth extrasUserIdentity, MfaFactor, MfaEnrollResponse, MfaChallengeResponse, AdminListUsersParams, AdminUserList, AdminCreateUserParams, AdminCreatedUser, AdminUpdateUserParams
ChannelsPostgresChangeEvent, PostgresChangeFilter, PostgresChangePayload, ChannelStatus, BroadcastFilter, PresenceFilter, BroadcastMessage, BroadcastReceived, RealtimeChannelOptions, PresenceEntry, PresenceState, PresenceJoinPayload, PresenceLeavePayload
Low-level realtimeRealtimeConnectOptions, RealtimeStatus, RealtimeStatusListener, RealtimeWatchEvent, RealtimeWatchOptions, IoFactory, SocketLike

A few source files export implementation symbols the root barrel does not re-export: SessionStore (session.ts), AuthEvent and AuthListener (types.ts), HttpClient / RequestOptions / toError (http.ts), encodeLiteral (postgrest.ts), and RealtimeModule (realtime.ts). Because packages/client/package.json exposes only ".", treat these as internals rather than package imports.