# How to Use Supabase in a Monorepo

- Tool: Supabase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+, Turborepo/Nx/pnpm workspaces
- Last updated: March 2026

## TL;DR

To use Supabase in a monorepo, create a shared package that exports a configured Supabase client and generated TypeScript types. Each app in the monorepo imports the client from the shared package instead of initializing its own. Store environment variables at the app level and pass them to the shared client factory function so each app can connect to different Supabase projects if needed.

## Setting Up Supabase in a Monorepo with Shared Types and Client

Monorepos let you share code between multiple apps — a web frontend, mobile app, and admin panel, for example. Instead of duplicating Supabase client initialization and types in every app, you can create a shared package that all apps import from. This tutorial shows you how to structure the shared package, generate types once and share them, and manage environment variables so each app connects to the right Supabase project.

## Before you start

- A monorepo set up with Turborepo, Nx, or pnpm workspaces
- A Supabase project with at least one table
- Supabase CLI installed globally or as a dev dependency
- Node.js 18+ and TypeScript configured

## Step-by-step guide

### 1. Create the shared Supabase package

In your monorepo, create a new package that will hold the Supabase client factory, generated types, and any shared utilities. With pnpm workspaces, create a folder under packages/ (for example, packages/supabase). Add a package.json with the package name and the @supabase/supabase-js dependency. This package will be the single source of truth for all Supabase interactions across your monorepo.

```
# Create the shared package directory
mkdir -p packages/supabase/src

# Initialize package.json
cd packages/supabase
npm init -y

# Install Supabase client
pnpm add @supabase/supabase-js
pnpm add -D typescript supabase
```

**Expected result:** A packages/supabase directory exists with @supabase/supabase-js installed.

### 2. Build the client factory function

Instead of hardcoding environment variables in the shared package, create a factory function that accepts the Supabase URL and key as arguments. Each app will call this factory with its own environment variables, letting you connect different apps to different Supabase projects or use different keys (anon vs service role). Export the factory function and the Database type from the package entry point.

```
// packages/supabase/src/client.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js'
import type { Database } from './types'

export function createSupabaseClient(
  url: string,
  anonKey: string
): SupabaseClient<Database> {
  return createClient<Database>(url, anonKey, {
    auth: {
      autoRefreshToken: true,
      persistSession: true,
    },
  })
}

export function createSupabaseAdmin(
  url: string,
  serviceRoleKey: string
): SupabaseClient<Database> {
  return createClient<Database>(url, serviceRoleKey, {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
  })
}

export type { Database } from './types'
```

**Expected result:** The shared package exports createSupabaseClient and createSupabaseAdmin factory functions.

### 3. Generate and share TypeScript types

Run supabase gen types typescript from the monorepo root or the shared package directory, pointing to your linked Supabase project. Save the output to the shared package so all apps get the same type definitions. Add a script to your package.json so anyone on the team can regenerate types when the schema changes. The generated types give you full autocomplete and type safety for all table operations across every app.

```
# Link to your Supabase project (run once)
supabase link --project-ref your-project-ref

# Generate types into the shared package
supabase gen types typescript --linked > packages/supabase/src/types.ts

# Add a script to the shared package's package.json
# "scripts": {
#   "gen-types": "supabase gen types typescript --linked > src/types.ts"
# }
```

**Expected result:** A types.ts file is generated in packages/supabase/src/ containing TypeScript types for all your tables.

### 4. Configure the package entry point and exports

Set up the package.json exports field so consuming apps can import the client factory and types cleanly. Configure TypeScript to compile the package, and make sure the main and types fields point to the right files. If you use Turborepo, add a build step to the turbo.json pipeline for this package.

```
// packages/supabase/package.json
{
  "name": "@myapp/supabase",
  "version": "1.0.0",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },
  "dependencies": {
    "@supabase/supabase-js": "^2.45.0"
  }
}

// packages/supabase/src/index.ts
export { createSupabaseClient, createSupabaseAdmin } from './client'
export type { Database } from './types'
```

**Expected result:** Other apps in the monorepo can import from @myapp/supabase and get full TypeScript support.

### 5. Use the shared client in each app

In each app (web, mobile, admin), add the shared package as a workspace dependency. Create a local supabase.ts file that calls the factory function with the app's environment variables. This keeps environment variables scoped to each app while sharing all the client logic and types. Each app can use different Supabase keys or even different projects.

```
// apps/web/.env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUz...

// apps/web/src/lib/supabase.ts
import { createSupabaseClient } from '@myapp/supabase'

export const supabase = createSupabaseClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// apps/web/src/app/page.tsx
import { supabase } from '@/lib/supabase'
import type { Database } from '@myapp/supabase'

type Product = Database['public']['Tables']['products']['Row']

const { data } = await supabase.from('products').select('*')
// data is typed as Product[] | null
```

**Expected result:** Each app initializes its own Supabase client using the shared factory function and gets full type safety from shared types.

## Complete code example

File: `packages/supabase/src/index.ts`

```typescript
// packages/supabase/src/index.ts
// Shared Supabase client factory for monorepo usage

import { createClient, SupabaseClient } from '@supabase/supabase-js'
import type { Database } from './types'

/**
 * Create a Supabase client for browser/client-side usage.
 * Uses the anon key and respects RLS policies.
 */
export function createSupabaseClient(
  url: string,
  anonKey: string
): SupabaseClient<Database> {
  return createClient<Database>(url, anonKey, {
    auth: {
      autoRefreshToken: true,
      persistSession: true,
    },
  })
}

/**
 * Create a Supabase admin client for server-side usage only.
 * Uses the service role key and bypasses all RLS policies.
 * NEVER use this in client-side code.
 */
export function createSupabaseAdmin(
  url: string,
  serviceRoleKey: string
): SupabaseClient<Database> {
  return createClient<Database>(url, serviceRoleKey, {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
  })
}

// Re-export types for consumer apps
export type { Database } from './types'

// Convenience type helpers
export type Tables<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Row']

export type InsertTables<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Insert']

export type UpdateTables<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Update']
```

## Common mistakes

- **Hardcoding the Supabase URL and anon key in the shared package instead of passing them as arguments** — undefined Fix: Use a factory function that accepts URL and key as parameters. Each app passes its own environment variables, allowing different apps to connect to different projects.
- **Forgetting to regenerate types after schema changes, causing type mismatches across apps** — undefined Fix: Add a gen-types script to the shared package and run it in CI after every migration. Use supabase gen types typescript --linked > src/types.ts.
- **Installing @supabase/supabase-js separately in each app, causing version conflicts** — undefined Fix: Install @supabase/supabase-js only in the shared package. Apps should depend on the shared package, which provides the client as a transitive dependency.

## Best practices

- Keep the Supabase client and types in a single shared package to avoid duplication
- Use a factory function pattern so each app can inject its own environment variables
- Regenerate TypeScript types in CI whenever database migrations are applied
- Export convenience type helpers like Tables<'products'> to simplify usage in consumer apps
- Use the anon key for client-side code and the service role key only in server-side API routes
- Scope the shared package name with your organization prefix for clarity in imports
- Pin @supabase/supabase-js to a specific version in the shared package to prevent unexpected updates

## Frequently asked questions

### Can I connect different apps in the monorepo to different Supabase projects?

Yes. The factory function pattern lets each app pass its own SUPABASE_URL and SUPABASE_ANON_KEY. Your web app could connect to a production project while your admin app connects to a staging project.

### Should I install @supabase/supabase-js in each app or only in the shared package?

Install it only in the shared package. Apps depend on the shared package, which provides the Supabase client as a transitive dependency. This prevents version conflicts and ensures all apps use the same client version.

### How do I regenerate types when my database schema changes?

Run supabase gen types typescript --linked > packages/supabase/src/types.ts from the monorepo root. Add this as a script in the shared package and include it in your CI pipeline after migrations.

### Does this approach work with Lovable or V0 projects?

Lovable and V0 are self-contained browser-based editors, so they do not directly support monorepo structures. However, if you export a Lovable or V0 project to GitHub, you can restructure it into a monorepo and use this shared package approach.

### How do I handle SSR with the shared Supabase client?

For SSR frameworks like Next.js, use the @supabase/ssr package alongside the shared client. Create a server-specific factory function that uses cookies instead of localStorage for session storage.

### Can RapidDev help set up a monorepo with shared Supabase infrastructure?

Yes. RapidDev can architect your monorepo structure, configure shared packages for Supabase client and types, set up CI pipelines for type generation, and implement proper environment variable management across all your apps.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-use-supabase-in-a-monorepo
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-use-supabase-in-a-monorepo
