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.
Prerequisites
- 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
Create the shared Supabase package
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.
1# Create the shared package directory2mkdir -p packages/supabase/src34# Initialize package.json5cd packages/supabase6npm init -y78# Install Supabase client9pnpm add @supabase/supabase-js10pnpm add -D typescript supabaseExpected result: A packages/supabase directory exists with @supabase/supabase-js installed.
Build the client factory function
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.
1// packages/supabase/src/client.ts2import { createClient, SupabaseClient } from '@supabase/supabase-js'3import type { Database } from './types'45export function createSupabaseClient(6 url: string,7 anonKey: string8): SupabaseClient<Database> {9 return createClient<Database>(url, anonKey, {10 auth: {11 autoRefreshToken: true,12 persistSession: true,13 },14 })15}1617export function createSupabaseAdmin(18 url: string,19 serviceRoleKey: string20): SupabaseClient<Database> {21 return createClient<Database>(url, serviceRoleKey, {22 auth: {23 autoRefreshToken: false,24 persistSession: false,25 },26 })27}2829export type { Database } from './types'Expected result: The shared package exports createSupabaseClient and createSupabaseAdmin factory functions.
Generate and share TypeScript types
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.
1# Link to your Supabase project (run once)2supabase link --project-ref your-project-ref34# Generate types into the shared package5supabase gen types typescript --linked > packages/supabase/src/types.ts67# Add a script to the shared package's package.json8# "scripts": {9# "gen-types": "supabase gen types typescript --linked > src/types.ts"10# }Expected result: A types.ts file is generated in packages/supabase/src/ containing TypeScript types for all your tables.
Configure the package entry point and exports
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.
1// packages/supabase/package.json2{3 "name": "@myapp/supabase",4 "version": "1.0.0",5 "main": "src/index.ts",6 "types": "src/index.ts",7 "exports": {8 ".": "./src/index.ts"9 },10 "dependencies": {11 "@supabase/supabase-js": "^2.45.0"12 }13}1415// packages/supabase/src/index.ts16export { createSupabaseClient, createSupabaseAdmin } from './client'17export type { Database } from './types'Expected result: Other apps in the monorepo can import from @myapp/supabase and get full TypeScript support.
Use the shared client in each app
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.
1// apps/web/.env.local2NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co3NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUz...45// apps/web/src/lib/supabase.ts6import { createSupabaseClient } from '@myapp/supabase'78export const supabase = createSupabaseClient(9 process.env.NEXT_PUBLIC_SUPABASE_URL!,10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!11)1213// apps/web/src/app/page.tsx14import { supabase } from '@/lib/supabase'15import type { Database } from '@myapp/supabase'1617type Product = Database['public']['Tables']['products']['Row']1819const { data } = await supabase.from('products').select('*')20// data is typed as Product[] | nullExpected result: Each app initializes its own Supabase client using the shared factory function and gets full type safety from shared types.
Complete working example
1// packages/supabase/src/index.ts2// Shared Supabase client factory for monorepo usage34import { createClient, SupabaseClient } from '@supabase/supabase-js'5import type { Database } from './types'67/**8 * Create a Supabase client for browser/client-side usage.9 * Uses the anon key and respects RLS policies.10 */11export function createSupabaseClient(12 url: string,13 anonKey: string14): SupabaseClient<Database> {15 return createClient<Database>(url, anonKey, {16 auth: {17 autoRefreshToken: true,18 persistSession: true,19 },20 })21}2223/**24 * Create a Supabase admin client for server-side usage only.25 * Uses the service role key and bypasses all RLS policies.26 * NEVER use this in client-side code.27 */28export function createSupabaseAdmin(29 url: string,30 serviceRoleKey: string31): SupabaseClient<Database> {32 return createClient<Database>(url, serviceRoleKey, {33 auth: {34 autoRefreshToken: false,35 persistSession: false,36 },37 })38}3940// Re-export types for consumer apps41export type { Database } from './types'4243// Convenience type helpers44export type Tables<T extends keyof Database['public']['Tables']> =45 Database['public']['Tables'][T]['Row']4647export type InsertTables<T extends keyof Database['public']['Tables']> =48 Database['public']['Tables'][T]['Insert']4950export type UpdateTables<T extends keyof Database['public']['Tables']> =51 Database['public']['Tables'][T]['Update']Common mistakes when using Supabase in a Monorepo
Why it's a problem: Hardcoding the Supabase URL and anon key in the shared package instead of passing them as arguments
How to avoid: 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.
Why it's a problem: Forgetting to regenerate types after schema changes, causing type mismatches across apps
How to avoid: 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.
Why it's a problem: Installing @supabase/supabase-js separately in each app, causing version conflicts
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Turborepo monorepo with a Next.js web app and a React Native mobile app. Both need to connect to the same Supabase project. Show me how to create a shared packages/supabase package with a client factory function and generated TypeScript types that both apps import from.
Generate a Supabase client factory module that accepts URL and key parameters, returns a typed SupabaseClient<Database>, and exports convenience type helpers for all tables. Include both a browser client (anon key, persistent session) and a server admin client (service role key, no session).
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation