@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 field | Behavior |
|---|---|
storage | SessionStorageAdapter; defaults to memoryStorage() |
autoRefresh | Revalidate once after an authenticated 401; defaults to true |
fetch | Fetch override; otherwise uses a bound globalThis.fetch |
headers | Extra headers merged into every request |
realtimeIoFactory | Custom 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
| Export | Purpose |
|---|---|
createClient | Factory for MotherbaseClient |
MotherbaseClient | Owns auth, storage, functions, realtime, query builders, RPC calls, and channels |
BackendError | Internal HTTP failure class also exposed for diagnostics |
memoryStorage | Process-local session adapter |
localStorageStorage | Browser localStorage adapter with an in-memory fallback |
cookieStorage | Browser cookie adapter with an in-memory fallback |
serverCookieStorage | Cookie adapter over batch getAll/setAll methods |
PostgrestQueryBuilder | Chainable and awaitable REST query implementation |
AuthClient | Auth methods under client.auth |
StorageClient / StorageBucket | Storage namespace and bucket handle |
FunctionsClient | Edge-function invoker under client.functions |
RealtimeChannel | Broadcast, presence, and Postgres-change channel |
MfaClient / AuthAdminClient | Classes behind client.auth.mfa and client.auth.admin |
FunctionsError, FunctionsHttpError, FunctionsRelayError, FunctionsFetchError | supabase-js error classes; invoke returns an instance in error rather than throwing |
keyValueStorage | Wraps a supabase-js key/value store as a SessionStorageAdapter |
normalizeSession / normalizeUser | Coerce a foreign session/user payload into this SDK's shape |
SESSION_COOKIE_KEY | Default 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.
| Method | Input | Result 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, password | Persists and emits the returned bearer session |
resetPasswordForEmail(email) | Email string | Requests a password-reset OTP; returns { error } |
resetPassword(args) | email, otp, password | Completes the OTP password reset; returns { error } |
signInWithOtp(credentials) | SignInWithOtpCredentials with email | Sends 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 options | Returns { provider, url } and redirects window.location.href unless skipped |
updateUser(attributes) | Profile data and/or password plus currentPassword | Updates profile, changes password, then calls getUser() |
deleteUser(attributes?) | Optional password, token, callbackURL | Calls self-service deletion and clears local session on success |
setSession({ access_token, refresh_token? }) | Externally obtained bearer token | Temporarily adopts, validates, then persists the session; refresh token is unused |
signOut() | None | Best-effort server sign-out; always clears local session and returns { error: null } |
getUser() | None | Fetches the current user from the server |
getSession() | None | Reads the hydrated local session without a network call |
refreshSession() | None | Revalidates the Better Auth session; keeps the access token, refreshes the user, or clears expired state |
onAuthStateChange(callback) | AuthChangeListener | Returns { 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
| Method | Behavior |
|---|---|
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
| Family | Methods |
|---|---|
| Comparison | eq, neq, gt, gte, lt, lte |
| Pattern | like, ilike, likeAllOf, likeAnyOf, ilikeAllOf, ilikeAnyOf |
| Set/null | in, is, match |
| Escape hatches | not, filter, or |
| Full-text | textSearch with plain, phrase, or websearch |
| Array/range/JSON | contains, containedBy, overlaps |
| Range position | rangeGt, rangeGte, rangeLt, rangeLte, rangeAdjacent |
| Result modifiers | order, limit, range, abortSignal, returns / overrideTypes, csv, geojson, explain, single, maybeSingle |
| Write guards | throwOnError (reject instead of resolving), maxAffected(n) (Prefer: handling=strict, max-affected=n), rollback (Prefer: tx=rollback) |
| Inspection | toPath, 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])
}| Method | Actual 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)forINSERT,UPDATE,DELETE, or*; schema defaults topublic.on('broadcast', { event }, callback), with*matching all broadcast events.on('presence', { event: 'sync' | 'join' | 'leave' }, callback).subscribe(callback?, timeoutMs?), returning the channel immediately; theChannelStatustype isSUBSCRIBED,TIMED_OUT,CLOSED, orCHANNEL_ERROR.send({ type: 'broadcast', event, payload? }),track(state),untrack(),presenceState(), andunsubscribe().
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:
| Area | Types |
|---|---|
| Core | ApiEnvelope, User, Tokens, Session, FileObject, FileVisibility, StorageUploadResponse, StorageRemoveResponse, UploadInput, SessionStorageAdapter, ClientOptions, MotherbaseResponse, MotherbaseError, AuthChangeEvent, AuthChangeListener |
| Auth | SignUpCredentials, SignInWithPasswordCredentials, SignInWithOtpCredentials, VerifyOtpParams, SignInWithOAuthCredentials, UpdateUserAttributes, DeleteUserAttributes, Subscription |
| Cookies | CookieMethods, CookieToSet, CookieOptions |
| REST | RpcOptions, PostgrestSingleBuilder, PostgrestTextBuilder, PostgrestExplainBuilder, PostgrestResponse, PostgrestPrimitive, PostgrestArrayOperand, PostgrestFilterOperator, PostgrestIsValue, PostgrestCount, TextSearchType, ReferencedTableOption, OrderOptions, SelectOptions, InsertOptions, UpsertOptions, MutateOptions, ExplainOptions |
| Database typing | GenericDatabase, GenericSchema, GenericTable, GenericView, GenericFunction, DefaultSchemaName, SchemaOf, Relations, RelationName, RowOf, InsertOf, UpdateOf, FunctionName, FunctionArgs, FunctionReturns, TypedQuery, TypedSingleQuery, SelectResult, ValidateSelect, UnknownSelectColumns, EmbeddedSelectSeam, ColumnName, FilterValue |
| Storage/functions | UploadOptions, UploadResult, ObjectInfo, ListOptions, FunctionInvokeAuth, FunctionInvokeOptions |
| Auth extras | UserIdentity, MfaFactor, MfaEnrollResponse, MfaChallengeResponse, AdminListUsersParams, AdminUserList, AdminCreateUserParams, AdminCreatedUser, AdminUpdateUserParams |
| Channels | PostgresChangeEvent, PostgresChangeFilter, PostgresChangePayload, ChannelStatus, BroadcastFilter, PresenceFilter, BroadcastMessage, BroadcastReceived, RealtimeChannelOptions, PresenceEntry, PresenceState, PresenceJoinPayload, PresenceLeavePayload |
| Low-level realtime | RealtimeConnectOptions, 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.
Related
- SDK reference — usage-oriented documentation.
- REST API and REST engine — what the query builder compiles to.
@motherbase/ssr— cookie-based sessions for server rendering.