Files
gh-hopeoverture-worldbuildi…/skills/supabase-prisma-database-management/references/supabase-integration.md
2025-11-29 18:46:53 +08:00

7.4 KiB

Supabase Integration with Prisma

Connection Configuration

Database URLs

Supabase provides two types of connection strings:

Direct Connection (Port 5432) - For migrations:

DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"

Pooled Connection (Port 6543) - For queries via pgBouncer:

DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"

Schema configuration:

datasource db {
  provider  = "postgresql"
  url       = env("DIRECT_URL")       // Pooled for app queries
  directUrl = env("DATABASE_URL")     // Direct for migrations
}

Why Two Connections?

  • Direct (5432): Supports all PostgreSQL features, required for migrations
  • Pooled (6543): Better performance, connection pooling, but limited features
  • Prisma uses direct for migrations, pooled for queries

Row Level Security (RLS) Considerations

Prisma vs Supabase Auth

Key concept: Prisma connects as the postgres user, which bypasses RLS policies.

This means:

  • Prisma can read/write all data regardless of RLS
  • Use Prisma in Server Components/Actions where you control access
  • RLS still protects data accessed via Supabase client

Using Prisma with RLS

For RLS to work with Prisma, you need to set the user context:

import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth/utils';

export async function getUserPosts() {
  const user = await getCurrentUser();

  // Set RLS context (requires custom configuration)
  await prisma.$executeRaw`SET LOCAL rls.user_id = ${user.id}`;

  // Now RLS policies can use current_setting('rls.user_id')
  const posts = await prisma.post.findMany();

  return posts;
}

Better approach: Use Prisma for admin operations, Supabase client for user operations:

// Admin operation - bypasses RLS
import { prisma } from '@/lib/prisma';
await prisma.post.findMany(); // Gets all posts

// User operation - respects RLS
import { createServerClient } from '@/lib/supabase/server';
const supabase = await createServerClient();
const { data } = await supabase.from('posts').select('*'); // Only user's posts

Schema Management

Prisma Manages Schema

Use Prisma as source of truth for schema:

# 1. Update schema.prisma
# 2. Generate migration
npx prisma migrate dev --name add_posts

# 3. Migration is applied to Supabase database

Supabase Features

Some Supabase features are managed outside Prisma:

RLS Policies - Define in Supabase dashboard or SQL:

CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);

Storage Buckets - Use Supabase dashboard/API

Realtime - Configure in Supabase dashboard

Edge Functions - Deploy separately

Hybrid Approach

  1. Use Prisma for schema, migrations, and admin operations
  2. Use Supabase client for user-facing operations with RLS
  3. Define RLS policies separately from Prisma

Working with Supabase Auth

Linking to auth.users

Don't create foreign key to auth.users (different schema):

model Profile {
  id    String @id @db.Uuid  // Same as auth.users.id
  email String @unique

  // No foreign key to auth.users - different schema
}

Create profile on signup via database trigger:

-- Create profile when user signs up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, email)
  VALUES (NEW.id, NEW.email);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW
  EXECUTE FUNCTION public.handle_new_user();

Syncing Profile Data

Keep profiles in sync with auth.users:

-- Update profile when email changes
CREATE OR REPLACE FUNCTION public.handle_user_update()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE public.profiles
  SET email = NEW.email
  WHERE id = NEW.id;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_updated
  AFTER UPDATE ON auth.users
  FOR EACH ROW
  WHEN (OLD.email IS DISTINCT FROM NEW.email)
  EXECUTE FUNCTION public.handle_user_update();

Migrations in Supabase

Local Development

  1. Pull current schema from Supabase:
npx prisma db pull
  1. Make changes to schema.prisma

  2. Create migration:

npx prisma migrate dev --name my_changes

Production Deployment

Apply migrations during deployment:

npx prisma migrate deploy

GitHub Actions example:

- name: Run Prisma migrations
  run: npx prisma migrate deploy
  env:
    DATABASE_URL: ${{ secrets.SUPABASE_DATABASE_URL }}

Migration History

Prisma creates _prisma_migrations table to track applied migrations. Don't modify this table.

Realtime with Prisma

Supabase Realtime works with Prisma-managed tables:

  1. Create table via Prisma migration
  2. Enable realtime in Supabase dashboard for specific tables
  3. Subscribe to changes via Supabase client
// Enable realtime on a table (in SQL editor or dashboard)
ALTER PUBLICATION supabase_realtime ADD TABLE posts;

// Subscribe to changes
const supabase = createClient();
const channel = supabase
  .channel('posts')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Change received!', payload);
    }
  )
  .subscribe();

Environment Setup

Development

# .env.local
DATABASE_URL="postgresql://postgres:postgres@localhost:54322/postgres"
DIRECT_URL="postgresql://postgres:postgres@localhost:54322/postgres"

Staging/Production

# .env.production
DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"

Best Practices

  1. Use Prisma for schema management - Single source of truth
  2. Use Supabase client for RLS-protected queries - User-facing operations
  3. Use Prisma for admin operations - Bulk updates, analytics
  4. Define RLS policies separately - Not managed by Prisma
  5. Use triggers for auth.users integration - Auto-create profiles
  6. Enable realtime selectively - Only on tables that need it
  7. Test migrations locally first - Use Supabase CLI for local dev
  8. Monitor connection pool - Use pooled connection for queries

Troubleshooting

Migration fails with permission error: Ensure DATABASE_URL uses postgres user with sufficient privileges.

RLS blocks Prisma queries: This is expected. Use Supabase client for RLS-protected data.

Connection pool exhausted: Use pooled connection (DIRECT_URL) for application queries.

Realtime not working: Check table is published (ALTER PUBLICATION supabase_realtime ADD TABLE tablename).

Auth user ID doesn't match profile: Ensure trigger exists and is executed on user creation.

Supabase CLI Integration

Use Supabase CLI for local development:

# Start local Supabase
npx supabase start

# Link to remote project
npx supabase link --project-ref your-project-ref

# Pull remote schema
npx supabase db pull

# Generate types for Supabase client
npx supabase gen types typescript --local > types/database.ts

Prisma and Supabase CLI can coexist:

  • Prisma for schema management and migrations
  • Supabase CLI for local development and type generation