Every quickstart below assumes a running project. Create one first with the Quickstart, then open Project Studio -> Connect for the exact API URL, WebSocket URL, and project keys.
Start with a generated app
For a ready-to-customize authentication flow, scaffold a Next.js or React app with the official templates:
npx create-motherbase-app my-app --template next
# or
npx create-motherbase-app my-app --template reactThe templates include sign in, sign up, password reset, OAuth callback, profile editing,
passkeys and MFA settings. They use @motherbase/ui and are still
ordinary application code after generation. See create-motherbase-app
for the setup options and template differences.
Common setup
npm install @motherbase/client
# Only if you use realtime channels:
npm install socket.io-clientTwo values are needed everywhere:
| Value | Where it comes from |
|---|---|
| API URL | Project Studio -> Connect, for example https://todo.example.com |
| Publishable key | Project Studio -> Connect; optional, sent as the apikey header |
The client is framework-agnostic. The only per-framework work is where the session is stored, chosen through ClientOptions.storage:
| Runtime | Adapter |
|---|---|
| Browser SPA | localStorageStorage() |
| Browser with server rendering | cookieStorage() |
| Server (SSR, route handlers, middleware) | serverCookieStorage(cookieMethods) |
| Mobile / other | Custom SessionStorageAdapter |
| Scripts and tests | memoryStorage() (default) |
Only @motherbase/ssr is framework-specific: it ships the Next.js cookie plumbing. Every other framework uses @motherbase/client directly, plus 10 lines of cookie glue when it renders on a server.
Next.js (App Router)
The only framework with a dedicated package.
npm install @motherbase/client @motherbase/ssr @motherbase/ui# .env.local
NEXT_PUBLIC_MOTHERBASE_URL=https://todo.example.com
NEXT_PUBLIC_MOTHERBASE_ANON_KEY=publishable-keyBrowser client, for Client Components:
// lib/motherbase/client.ts
'use client'
import { createBrowserClient } from '@motherbase/ssr'
export const motherbase = createBrowserClient(
process.env.NEXT_PUBLIC_MOTHERBASE_URL!,
process.env.NEXT_PUBLIC_MOTHERBASE_ANON_KEY,
)Server client, for Server Components, Route Handlers, and Server Actions:
// lib/motherbase/server.ts
import { cookies } from 'next/headers'
import { createServerClient } from '@motherbase/ssr'
import type { CookieToSet } from '@motherbase/ssr'
export async function createMotherbaseServerClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_MOTHERBASE_URL!,
process.env.NEXT_PUBLIC_MOTHERBASE_ANON_KEY,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet: CookieToSet[]) => {
try {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options)
}
} catch {
// Server Components cannot write cookies; middleware refreshes instead.
}
},
},
},
)
}Middleware keeps the session fresh and owns the Set-Cookie write:
// middleware.ts
import type { NextRequest } from 'next/server'
import { updateSession } from '@motherbase/ssr'
export function middleware(request: NextRequest) {
return updateSession(
request,
process.env.NEXT_PUBLIC_MOTHERBASE_URL!,
process.env.NEXT_PUBLIC_MOTHERBASE_ANON_KEY,
)
}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }First query, in a Server Component:
import { createMotherbaseServerClient } from '@/lib/motherbase/server'
export default async function ProductsPage() {
const client = await createMotherbaseServerClient()
const { data: products, error } = await client
.from<{ id: string; name: string }>('products')
.select('id,name')
if (error) return <p>{error.message}</p>
return <pre>{JSON.stringify(products, null, 2)}</pre>
}Return the NextResponse produced by updateSession. Replacing it with a new response drops the refreshed or cleared session cookie.
Details: @motherbase/ssr.
For complete auth screens instead of assembling each form, see
@motherbase/ui.
React (Vite SPA)
No server, so localStorage is enough.
# .env
VITE_MOTHERBASE_URL=https://todo.example.com
VITE_MOTHERBASE_ANON_KEY=publishable-key// src/motherbase.ts
import { createClient, localStorageStorage } from '@motherbase/client'
export const motherbase = createClient(
import.meta.env.VITE_MOTHERBASE_URL,
import.meta.env.VITE_MOTHERBASE_ANON_KEY,
{ storage: localStorageStorage() },
)hydrate() restores the persisted session once; auth state changes come from onAuthStateChange:
import { useEffect, useState } from 'react'
import type { User } from '@motherbase/client'
import { motherbase } from './motherbase'
export function useUser() {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
motherbase.hydrate().then(async () => {
const { data } = await motherbase.auth.getUser()
setUser(data?.user ?? null)
})
const { data } = motherbase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
})
return () => data.subscription.unsubscribe()
}, [])
return user
}await motherbase.auth.signInWithPassword({ email, password })
const { data: rows, error } = await motherbase.from('products').select('*').limit(20)Add the app origin (http://localhost:5173) to the project's corsOrigins in the Platform Console.
Vue 3 (Vite SPA)
Identical wiring; the client is exported from a plain module and used from setup().
// src/motherbase.ts
import { createClient, localStorageStorage } from '@motherbase/client'
export const motherbase = createClient(
import.meta.env.VITE_MOTHERBASE_URL,
import.meta.env.VITE_MOTHERBASE_ANON_KEY,
{ storage: localStorageStorage() },
)// src/composables/useUser.ts
import { onUnmounted, ref } from 'vue'
import type { User } from '@motherbase/client'
import { motherbase } from '../motherbase'
export function useUser() {
const user = ref<User | null>(null)
motherbase.hydrate().then(async () => {
const { data } = await motherbase.auth.getUser()
user.value = data?.user ?? null
})
const { data } = motherbase.auth.onAuthStateChange((_event, session) => {
user.value = session?.user ?? null
})
onUnmounted(() => data.subscription.unsubscribe())
return user
}Nuxt
Nuxt renders on both sides, so pick the adapter per side. Cookie storage on the browser makes the session readable by the server on the next request.
// plugins/motherbase.client.ts
import { createClient, cookieStorage } from '@motherbase/client'
export default defineNuxtPlugin(() => {
const { public: config } = useRuntimeConfig()
const client = createClient(config.motherbaseUrl, config.motherbaseAnonKey, {
storage: cookieStorage(),
})
return { provide: { motherbase: client } }
})Server routes build a client per request from the H3 event cookies:
// server/utils/motherbase.ts
import type { H3Event } from 'h3'
import { createClient, serverCookieStorage } from '@motherbase/client'
export function motherbaseFromEvent(event: H3Event) {
const { public: config } = useRuntimeConfig(event)
return createClient(config.motherbaseUrl, config.motherbaseAnonKey, {
storage: serverCookieStorage({
getAll: () =>
Object.entries(parseCookies(event)).map(([name, value]) => ({ name, value })),
setAll: cookies => {
for (const { name, value, options } of cookies) {
setCookie(event, name, value, options)
}
},
}),
})
}// server/api/products.get.ts
export default defineEventHandler(async event => {
const client = motherbaseFromEvent(event)
const { data, error } = await client.from('products').select('id,name')
if (error) throw createError({ statusCode: error.status ?? 500, message: error.message })
return data
})SvelteKit
event.cookies already matches the CookieMethods shape, so the server client is a few lines in hooks.server.ts.
// src/hooks.server.ts
import { createClient, serverCookieStorage } from '@motherbase/client'
import { PUBLIC_MOTHERBASE_URL, PUBLIC_MOTHERBASE_ANON_KEY } from '$env/static/public'
import type { Handle } from '@sveltejs/kit'
export const handle: Handle = async ({ event, resolve }) => {
event.locals.motherbase = createClient(
PUBLIC_MOTHERBASE_URL,
PUBLIC_MOTHERBASE_ANON_KEY,
{
storage: serverCookieStorage({
getAll: () => event.cookies.getAll(),
setAll: cookies => {
for (const { name, value, options } of cookies) {
event.cookies.set(name, value, { path: '/', ...options })
}
},
}),
},
)
return resolve(event)
}// src/routes/products/+page.server.ts
export async function load({ locals }) {
const { data, error } = await locals.motherbase.from('products').select('id,name')
if (error) return { products: [], message: error.message }
return { products: data }
}In the browser, use cookieStorage() so both sides read the same motherbase.session cookie:
// src/lib/motherbase.ts
import { createClient, cookieStorage } from '@motherbase/client'
import { PUBLIC_MOTHERBASE_URL, PUBLIC_MOTHERBASE_ANON_KEY } from '$env/static/public'
export const motherbase = createClient(PUBLIC_MOTHERBASE_URL, PUBLIC_MOTHERBASE_ANON_KEY, {
storage: cookieStorage(),
})The shared session cookie is not HttpOnly, because browser auth code must write it. Treat the serialized session as sensitive and keep the application protected against script injection.
Expo / React Native
No localStorage and no cookies, so supply an async adapter over AsyncStorage.
npm install @motherbase/client @react-native-async-storage/async-storage// lib/motherbase.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@motherbase/client'
export const motherbase = createClient(
process.env.EXPO_PUBLIC_MOTHERBASE_URL!,
process.env.EXPO_PUBLIC_MOTHERBASE_ANON_KEY,
{
storage: {
async get() {
const raw = await AsyncStorage.getItem('motherbase.session')
return raw ? JSON.parse(raw) : null
},
async set(session) {
if (session) await AsyncStorage.setItem('motherbase.session', JSON.stringify(session))
else await AsyncStorage.removeItem('motherbase.session')
},
},
},
)Call await motherbase.hydrate() at startup to restore the stored session before the first authenticated request.
For OAuth, pass skipBrowserRedirect: true and open the returned URL yourself:
const { data } = await motherbase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: 'myapp://auth/callback', skipBrowserRedirect: true },
})
// open data.url in an in-app browser, then handle the callback deep linkNode / vanilla TypeScript
Scripts, workers, and non-browser backends keep the default in-memory session.
import { createClient } from '@motherbase/client'
const client = createClient(process.env.MOTHERBASE_URL!, process.env.MOTHERBASE_ANON_KEY)
await client.auth.signInWithPassword({
email: process.env.MOTHERBASE_EMAIL!,
password: process.env.MOTHERBASE_PASSWORD!,
})
const { data, error } = await client.from('products').select('id,name').limit(50)Node 18+ provides fetch. On older runtimes, pass one explicitly: createClient(url, key, { fetch: myFetch }). The constructor throws when no fetch implementation is available.
A shared process-wide client means a shared session. For a request-scoped identity, create one client per request, or forward the caller's bearer token with setSession({ access_token }).
Realtime, in any framework
Realtime is the same code everywhere once socket.io-client is installed:
const channel = client
.channel('room:products')
.on('postgres_changes', { event: '*', schema: 'public', table: 'products' }, payload => {
console.log(payload.eventType, payload.new, payload.old)
})
.subscribe(status => console.log(status))
// later
await channel.unsubscribe()Subscribe from the browser or the device, not from a Server Component. Unsubscribe on unmount.
Checklist
- Project created in the Platform Console.
- App origin listed in the project's
corsOrigins. - API URL and publishable key read from Project Studio -> Connect.
- Storage adapter matching the runtime.
- RLS policies enabled for the tables being queried;
/rest/v1blocks any operation no enabled policy allows.
Related
- SDK reference — full method list.
@motherbase/ssr— the Next.js helpers in detail.- Auth — providers, sessions, and OAuth setup.
- RLS — why an empty result may be a policy, not a bug.
- Todo app example — end-to-end walkthrough.