Browse documentation

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 react

The 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-client

Two values are needed everywhere:

ValueWhere it comes from
API URLProject Studio -> Connect, for example https://todo.example.com
Publishable keyProject 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:

RuntimeAdapter
Browser SPAlocalStorageStorage()
Browser with server renderingcookieStorage()
Server (SSR, route handlers, middleware)serverCookieStorage(cookieMethods)
Mobile / otherCustom SessionStorageAdapter
Scripts and testsmemoryStorage() (default)
Note

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-key

Browser 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>
}
Warning

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(),
})
Warning

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 link

Node / 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.

Note

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

  1. Project created in the Platform Console.
  2. App origin listed in the project's corsOrigins.
  3. API URL and publishable key read from Project Studio -> Connect.
  4. Storage adapter matching the runtime.
  5. RLS policies enabled for the tables being queried; /rest/v1 blocks any operation no enabled policy allows.