Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:53 +08:00
commit f2cccfe864
10 changed files with 1544 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "supabase-prisma-database-management",
"description": "This skill should be used when managing database schema, migrations, and seed data using Prisma ORM with Supabase PostgreSQL. Apply when setting up Prisma with Supabase, creating migrations, seeding data, configuring shadow database for migration preview, adding schema validation to CI, or managing database changes across environments.",
"version": "1.0.0",
"author": {
"name": "Hope Overture",
"email": "support@worldbuilding-app-skills.dev"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# supabase-prisma-database-management
This skill should be used when managing database schema, migrations, and seed data using Prisma ORM with Supabase PostgreSQL. Apply when setting up Prisma with Supabase, creating migrations, seeding data, configuring shadow database for migration preview, adding schema validation to CI, or managing database changes across environments.

69
plugin.lock.json Normal file
View File

@@ -0,0 +1,69 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/supabase-prisma-database-management",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "3cc7577803aaaca115b5955b2bf32e8f4da3094e",
"treeHash": "b7557067bf121905c75a0883f13557acc850a50f94454eb857ff2737011b8f59",
"generatedAt": "2025-11-28T10:17:32.267125Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "supabase-prisma-database-management",
"description": "This skill should be used when managing database schema, migrations, and seed data using Prisma ORM with Supabase PostgreSQL. Apply when setting up Prisma with Supabase, creating migrations, seeding data, configuring shadow database for migration preview, adding schema validation to CI, or managing database changes across environments.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "fa0878b307d2e1e94de257b7aa12f6bc424f365d951aba156e8545ff45aa241e"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "a818c92d999e5ad5f7d2af4e92ae2bc8e031473b1bbbf62a473e060412a7bac6"
},
{
"path": "skills/supabase-prisma-database-management/SKILL.md",
"sha256": "bf43feb2bb599f0fed552389315fbc46c2330d944ba2e5a3c411124be63a2dec"
},
{
"path": "skills/supabase-prisma-database-management/references/prisma-best-practices.md",
"sha256": "b606e0841fa25c10f4098d144e6026092c6f9fe40099dbf42db725ce33e6e218"
},
{
"path": "skills/supabase-prisma-database-management/references/supabase-integration.md",
"sha256": "ac7d02a89efa6e5b52f6c880dc1bc53b054eafb8654b7c0ce7edbccd9618fe5e"
},
{
"path": "skills/supabase-prisma-database-management/assets/github-workflows-schema-check.yml",
"sha256": "f5a6d29137b6e4ba81b77e4b2dd68d74efbbf7465575828345fe3e6b17c46ee4"
},
{
"path": "skills/supabase-prisma-database-management/assets/seed.ts",
"sha256": "446feea73ce69d77d62956c5f2b44074fdbcc802437a7af576a3a929cf934e50"
},
{
"path": "skills/supabase-prisma-database-management/assets/prisma-client.ts",
"sha256": "cfbf200b587d57d1921580a2ec0cfd53454baac51383e7506c44219f3a39bcae"
},
{
"path": "skills/supabase-prisma-database-management/assets/example-schema.prisma",
"sha256": "bc104bc1e77700e0d20602eb4df798f754450ecf7d4cc3e31707400f6109e201"
}
],
"dirSha256": "b7557067bf121905c75a0883f13557acc850a50f94454eb857ff2737011b8f59"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,393 @@
---
name: supabase-prisma-database-management
description: This skill should be used when managing database schema, migrations, and seed data using Prisma ORM with Supabase PostgreSQL. Apply when setting up Prisma with Supabase, creating migrations, seeding data, configuring shadow database for migration preview, adding schema validation to CI, or managing database changes across environments.
---
# Supabase + Prisma Database Management
## Overview
Manage database schema, migrations, and seed data using Prisma ORM with Supabase PostgreSQL, including shadow database configuration, seed files, and automated schema checks in CI.
## Installation and Setup
### 1. Install Prisma
Install Prisma CLI and client:
```bash
npm install -D prisma
npm install @prisma/client
```
### 2. Initialize Prisma
Initialize Prisma in your project:
```bash
npx prisma init
```
This creates:
- `prisma/schema.prisma` - Database schema definition
- `.env` - Environment variables (add `DATABASE_URL`)
### 3. Configure Supabase Connection
Get your Supabase database URL from:
- Supabase Dashboard > Project Settings > Database > Connection String > URI
Add to `.env`:
```env
# Transaction pooler for Prisma migrations
DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
# Session pooler for queries (with pgBouncer)
DIRECT_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
```
Update `prisma/schema.prisma` to use both URLs:
```prisma
datasource db {
provider = "postgresql"
url = env("DIRECT_URL")
directUrl = env("DATABASE_URL")
}
```
**Why two URLs?**
- `DATABASE_URL`: Direct connection for migrations (required)
- `DIRECT_URL`: Pooled connection for application queries (optional, better performance)
### 4. Configure Shadow Database (Required for Migrations)
For migration preview and validation, configure a shadow database in `prisma/schema.prisma`:
```prisma
datasource db {
provider = "postgresql"
url = env("DIRECT_URL")
directUrl = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
```
Add to `.env`:
```env
SHADOW_DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
```
**Note**: Supabase free tier allows using the same database for shadow. For production, use a separate database.
## Schema Definition
### 1. Define Your Schema
Edit `prisma/schema.prisma` using the example from `assets/example-schema.prisma`. This example includes:
- User profiles with auth integration
- Timestamps with `@default(now())` and `@updatedAt`
- Relations between entities
- Indexes for performance
- Unique constraints
Key Prisma features:
- `@id @default(uuid())` - Auto-generated UUIDs
- `@default(now())` - Automatic timestamps
- `@updatedAt` - Auto-update on modification
- `@@index([field])` - Database indexes
- `@relation` - Define relationships
### 2. Link to Supabase Auth
To integrate with Supabase Auth, reference the `auth.users` table:
```prisma
model Profile {
id String @id @db.Uuid
email String @unique
// Other fields...
// This doesn't create a foreign key, just documents the relationship
// The actual user exists in auth.users (managed by Supabase)
}
```
**Important**: Don't create a foreign key to `auth.users` as it's in a different schema. Handle the relationship in application logic.
## Migrations
### 1. Create Migration
After defining/modifying schema, create a migration:
```bash
npx prisma migrate dev --name add_profiles_table
```
This:
- Generates SQL migration in `prisma/migrations/`
- Applies migration to database
- Regenerates Prisma Client
- Runs seed script (if configured)
### 2. Review Migration SQL
Always review generated SQL in `prisma/migrations/[timestamp]_[name]/migration.sql`:
```sql
-- CreateTable
CREATE TABLE "Profile" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
-- ...
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Profile_email_key" ON "Profile"("email");
```
Make manual adjustments if needed before applying to production.
### 3. Apply Migrations in Production
For production deployments:
```bash
npx prisma migrate deploy
```
This applies pending migrations without prompts or seeds.
**CI/CD Integration**: Add to your deployment pipeline:
```yaml
# Example GitHub Actions step
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
```
### 4. Reset Database (Development Only)
To reset database to clean state:
```bash
npx prisma migrate reset
```
This:
- Drops database
- Creates database
- Applies all migrations
- Runs seed script
**Warning**: This deletes all data. Only use in development.
## Seeding Data
### 1. Create Seed Script
Create `prisma/seed.ts` using the template from `assets/seed.ts`. This script:
- Uses Prisma Client to insert data
- Creates initial users, settings, or reference data
- Can be run manually or after migrations
- Supports idempotent operations (safe to run multiple times)
### 2. Configure Seed in package.json
Add seed configuration to `package.json`:
```json
{
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
}
```
Install ts-node for TypeScript execution:
```bash
npm install -D ts-node
```
### 3. Run Seed Manually
Execute seed script:
```bash
npx prisma db seed
```
Seed runs automatically after `prisma migrate dev` and `prisma migrate reset`.
### 4. Idempotent Seeding
Make seeds safe to run multiple times using upsert:
```typescript
await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {}, // No updates if exists
create: {
email: 'admin@example.com',
name: 'Admin User',
},
});
```
## Prisma Client Usage
### 1. Generate Client
After schema changes, regenerate Prisma Client:
```bash
npx prisma generate
```
This updates `node_modules/@prisma/client` with types matching your schema.
### 2. Use in Next.js Server Components
Create a Prisma client singleton using `assets/prisma-client.ts`:
```typescript
import { prisma } from '@/lib/prisma';
export default async function UsersPage() {
const users = await prisma.profile.findMany();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
```
### 3. Use in Server Actions
```typescript
'use server';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function createProfile(formData: FormData) {
const name = formData.get('name') as string;
await prisma.profile.create({
data: {
name,
email: formData.get('email') as string,
},
});
revalidatePath('/profiles');
}
```
## CI/CD Integration
### 1. Add Schema Validation to CI
Create `.github/workflows/schema-check.yml` using the template from `assets/github-workflows-schema-check.yml`. This workflow:
- Runs on pull requests
- Validates schema syntax
- Checks for migration drift
- Ensures migrations are generated
- Verifies Prisma Client generation
### 2. Migration Deployment
Add migration step to deployment workflow:
```yaml
- name: Apply database migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
```
### 3. Environment-Specific Databases
Use different database URLs for each environment:
```env
# Development
DATABASE_URL="postgresql://localhost:5432/dev"
# Staging
DATABASE_URL="postgresql://staging-db.supabase.co:5432/postgres"
# Production
DATABASE_URL="postgresql://prod-db.supabase.co:5432/postgres"
```
## Best Practices
### Schema Design
1. **Use UUIDs for IDs**: Better for distributed systems
2. **Add Timestamps**: Track `createdAt` and `updatedAt`
3. **Define Indexes**: Improve query performance on filtered fields
4. **Use Enums**: Type-safe status/role fields
5. **Validate at DB Level**: Use unique constraints and checks
### Migration Management
1. **Review Before Applying**: Always check generated SQL
2. **Name Descriptively**: Use clear migration names
3. **Keep Atomic**: One logical change per migration
4. **Test Locally First**: Verify migrations work before production
5. **Never Modify Applied Migrations**: Create new ones instead
### Prisma Client
1. **Use Singleton Pattern**: Prevent connection exhaustion
2. **Close in Serverless**: Disconnect after operations
3. **Type Everything**: Leverage Prisma's TypeScript types
4. **Use Select**: Only fetch needed fields
5. **Batch Operations**: Use `createMany`, `updateMany` for bulk ops
## Troubleshooting
**Migration fails with "relation already exists"**: Reset development database with `npx prisma migrate reset`. For production, manually fix conflicts.
**Prisma Client out of sync**: Run `npx prisma generate` after schema changes.
**Connection pool exhausted**: Use connection pooling via `DIRECT_URL` with pgBouncer.
**Shadow database errors**: Ensure shadow database URL is correct and accessible. For Supabase free tier, same DB can be used.
**Type errors after schema changes**: Restart TypeScript server in IDE after `prisma generate`.
## Resources
### scripts/
No executable scripts needed for this skill.
### references/
- `prisma-best-practices.md` - Comprehensive guide to Prisma patterns, performance optimization, and common pitfalls
- `supabase-integration.md` - Specific considerations for using Prisma with Supabase, including RLS integration
### assets/
- `example-schema.prisma` - Complete schema example with common patterns (auth, timestamps, relations, indexes)
- `seed.ts` - Idempotent seed script template for initial data
- `prisma-client.ts` - Singleton Prisma Client for Next.js to prevent connection exhaustion
- `github-workflows-schema-check.yml` - CI workflow for schema validation and migration checks

View File

@@ -0,0 +1,132 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DIRECT_URL") // Pooled connection for queries
directUrl = env("DATABASE_URL") // Direct connection for migrations
shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // For migration preview
}
// User profile - links to Supabase auth.users
model Profile {
id String @id @default(uuid()) @db.Uuid
email String @unique
name String?
avatarUrl String? @map("avatar_url")
bio String? @db.Text
role UserRole @default(USER)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
posts Post[]
comments Comment[]
@@index([email])
@@map("profiles")
}
// User roles enum
enum UserRole {
USER
MODERATOR
ADMIN
}
// Blog post example
model Post {
id String @id @default(uuid()) @db.Uuid
title String
slug String @unique
content String @db.Text
excerpt String? @db.Text
published Boolean @default(false)
publishedAt DateTime? @map("published_at")
authorId String @map("author_id") @db.Uuid
author Profile @relation(fields: [authorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
comments Comment[]
tags PostTag[]
@@index([authorId])
@@index([slug])
@@index([published, publishedAt])
@@map("posts")
}
// Comment model
model Comment {
id String @id @default(uuid()) @db.Uuid
content String @db.Text
postId String @map("post_id") @db.Uuid
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
authorId String @map("author_id") @db.Uuid
author Profile @relation(fields: [authorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([postId])
@@index([authorId])
@@map("comments")
}
// Tag model
model Tag {
id String @id @default(uuid()) @db.Uuid
name String @unique
slug String @unique
posts PostTag[]
@@index([slug])
@@map("tags")
}
// Many-to-many relation between posts and tags
model PostTag {
postId String @map("post_id") @db.Uuid
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tagId String @map("tag_id") @db.Uuid
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
assignedAt DateTime @default(now()) @map("assigned_at")
@@id([postId, tagId])
@@map("post_tags")
}
// Settings model (singleton pattern)
model Settings {
id String @id @default(uuid()) @db.Uuid
siteName String @map("site_name")
siteUrl String @map("site_url")
description String? @db.Text
// SEO
metaTitle String? @map("meta_title")
metaDescription String? @map("meta_description")
// Features
enableComments Boolean @default(true) @map("enable_comments")
enableRegistration Boolean @default(true) @map("enable_registration")
maintenanceMode Boolean @default(false) @map("maintenance_mode")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("settings")
}

View File

@@ -0,0 +1,62 @@
name: Database Schema Check
on:
pull_request:
paths:
- 'prisma/**'
- '.github/workflows/schema-check.yml'
jobs:
schema-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate Prisma schema
run: npx prisma validate
- name: Format Prisma schema
run: npx prisma format --check
- name: Generate Prisma Client
run: npx prisma generate
- name: Check for migration drift
run: |
# This checks if schema.prisma matches the current migrations
# If there are changes without migrations, this will fail
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma \
--script > migration-diff.sql
if [ -s migration-diff.sql ]; then
echo "Schema changes detected without migration!"
echo "Run 'npx prisma migrate dev' to create a migration"
cat migration-diff.sql
exit 1
fi
continue-on-error: true
- name: Comment PR with schema changes
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '[WARN] Schema changes detected. Please generate a migration with `npx prisma migrate dev`'
})

View File

@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

View File

@@ -0,0 +1,158 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting seed...');
// Create or update settings (singleton)
const settings = await prisma.settings.upsert({
where: { id: '00000000-0000-0000-0000-000000000001' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000001',
siteName: 'My Worldbuilding App',
siteUrl: 'https://example.com',
description: 'A platform for building immersive worlds',
metaTitle: 'Worldbuilding App - Create Amazing Worlds',
metaDescription: 'Build, manage, and share your fictional worlds',
enableComments: true,
enableRegistration: true,
maintenanceMode: false,
},
});
console.log('[OK] Settings created:', settings.siteName);
// Create sample tags
const tags = await Promise.all([
prisma.tag.upsert({
where: { slug: 'worldbuilding' },
update: {},
create: {
name: 'Worldbuilding',
slug: 'worldbuilding',
},
}),
prisma.tag.upsert({
where: { slug: 'character-development' },
update: {},
create: {
name: 'Character Development',
slug: 'character-development',
},
}),
prisma.tag.upsert({
where: { slug: 'lore' },
update: {},
create: {
name: 'Lore',
slug: 'lore',
},
}),
]);
console.log(`[OK] Created ${tags.length} tags`);
// Create sample user profile (only if doesn't exist)
const sampleUser = await prisma.profile.upsert({
where: { email: 'demo@example.com' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000002',
email: 'demo@example.com',
name: 'Demo User',
bio: 'Sample user for testing',
role: 'USER',
},
});
console.log('[OK] Sample user created:', sampleUser.email);
// Create sample post
const samplePost = await prisma.post.upsert({
where: { slug: 'getting-started-with-worldbuilding' },
update: {},
create: {
title: 'Getting Started with Worldbuilding',
slug: 'getting-started-with-worldbuilding',
excerpt: 'Learn the basics of creating your own fictional world',
content: `
# Getting Started with Worldbuilding
Worldbuilding is the process of constructing an imaginary world, sometimes associated with a whole fictional universe.
## Key Elements
1. **Geography**: Define the physical layout of your world
2. **History**: Create a timeline of major events
3. **Culture**: Develop societies and their customs
4. **Magic/Technology**: Establish the rules of your world
Start small and expand gradually. You don't need to build everything at once!
`.trim(),
published: true,
publishedAt: new Date(),
authorId: sampleUser.id,
},
});
console.log('[OK] Sample post created:', samplePost.title);
// Link tags to post
await prisma.postTag.upsert({
where: {
postId_tagId: {
postId: samplePost.id,
tagId: tags[0].id,
},
},
update: {},
create: {
postId: samplePost.id,
tagId: tags[0].id,
},
});
await prisma.postTag.upsert({
where: {
postId_tagId: {
postId: samplePost.id,
tagId: tags[2].id,
},
},
update: {},
create: {
postId: samplePost.id,
tagId: tags[2].id,
},
});
console.log('[OK] Tags linked to post');
// Create sample comment
await prisma.comment.upsert({
where: { id: '00000000-0000-0000-0000-000000000003' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000003',
content: 'Great introduction! Looking forward to more posts.',
postId: samplePost.id,
authorId: sampleUser.id,
},
});
console.log('[OK] Sample comment created');
console.log('🎉 Seed completed successfully!');
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error('[ERROR] Seed failed:', e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -0,0 +1,412 @@
# Prisma Best Practices
## Schema Design
### Naming Conventions
**Models**: PascalCase, singular
```prisma
model User { } // [OK] Good
model users { } // [ERROR] Bad
```
**Fields**: camelCase
```prisma
model User {
createdAt DateTime // [OK] Good
created_at DateTime // [ERROR] Bad (unless mapping to existing DB)
}
```
**Database names**: snake_case with `@@map` and `@map`
```prisma
model Profile {
avatarUrl String @map("avatar_url")
@@map("profiles")
}
```
### ID Fields
**Prefer UUIDs for distributed systems:**
```prisma
id String @id @default(uuid()) @db.Uuid
```
**Use auto-increment for simple cases:**
```prisma
id Int @id @default(autoincrement())
```
### Timestamps
Always include creation and update timestamps:
```prisma
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
```
### Indexes
Add indexes on:
- Foreign keys (automatically indexed)
- Frequently queried fields
- Unique constraints
- Compound queries
```prisma
model Post {
authorId String @db.Uuid
published Boolean
publishedAt DateTime?
@@index([authorId])
@@index([published, publishedAt])
}
```
### Relations
**One-to-many:**
```prisma
model User {
posts Post[]
}
model Post {
authorId String @db.Uuid
author User @relation(fields: [authorId], references: [id])
@@index([authorId])
}
```
**Many-to-many:**
```prisma
model Post {
tags PostTag[]
}
model Tag {
posts PostTag[]
}
model PostTag {
postId String @db.Uuid
post Post @relation(fields: [postId], references: [id])
tagId String @db.Uuid
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}
```
**Cascade deletes:**
```prisma
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
```
## Query Optimization
### Select Only Needed Fields
```typescript
// [ERROR] Bad - fetches all fields
const users = await prisma.user.findMany();
// [OK] Good - only fetch what you need
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
```
### Use Include Wisely
```typescript
// [ERROR] Bad - deep nesting can be slow
const posts = await prisma.post.findMany({
include: {
author: {
include: {
profile: {
include: {
settings: true,
},
},
},
},
},
});
// [OK] Good - only include what you display
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
},
},
},
});
```
### Pagination
Always paginate large result sets:
```typescript
// Offset pagination
const posts = await prisma.post.findMany({
skip: 20,
take: 10,
});
// Cursor-based pagination (better for large datasets)
const posts = await prisma.post.findMany({
take: 10,
cursor: {
id: lastPostId,
},
});
```
### Batch Operations
Use batch operations for bulk inserts/updates:
```typescript
// [OK] Single query instead of N queries
await prisma.user.createMany({
data: [
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
],
skipDuplicates: true,
});
```
## Connection Management
### Singleton Pattern (Next.js)
Prevent connection exhaustion in development:
```typescript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
```
### Connection Pooling
Use Supabase connection pooler for better performance:
```prisma
datasource db {
provider = "postgresql"
url = env("DIRECT_URL") // Pooled connection
directUrl = env("DATABASE_URL") // Direct for migrations
}
```
## Migration Best Practices
### 1. Review Generated SQL
Always check `migration.sql` before applying:
```bash
npx prisma migrate dev --name add_users --create-only
# Review prisma/migrations/.../migration.sql
npx prisma migrate dev
```
### 2. Atomic Migrations
One logical change per migration:
- [OK] Good: "add_user_roles"
- [ERROR] Bad: "add_users_and_posts_and_comments"
### 3. Production Migrations
Use `migrate deploy` in production:
```bash
npx prisma migrate deploy
```
Never use `migrate dev` or `migrate reset` in production.
### 4. Handle Data Migrations
For complex data transformations, use multi-step migrations:
```sql
-- Step 1: Add new column with default
ALTER TABLE "User" ADD COLUMN "full_name" TEXT;
-- Step 2: Populate from existing data
UPDATE "User" SET "full_name" = "first_name" || ' ' || "last_name";
-- Step 3: Make non-nullable
ALTER TABLE "User" ALTER COLUMN "full_name" SET NOT NULL;
```
### 5. Rollback Strategy
Migrations can't be rolled back automatically. Plan for:
- Backup before major migrations
- Keep old columns temporarily
- Deploy in stages
## Error Handling
### Handle Unique Constraint Violations
```typescript
try {
await prisma.user.create({
data: { email: 'test@example.com' },
});
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('Email already exists');
}
throw error;
}
```
### Use Transactions
For operations that must succeed or fail together:
```typescript
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'test@example.com' },
});
await tx.profile.create({
data: {
userId: user.id,
bio: 'New user',
},
});
});
```
## TypeScript Integration
### Leverage Generated Types
```typescript
import { Prisma } from '@prisma/client';
// Use generated types for input
type UserCreateInput = Prisma.UserCreateInput;
// Type-safe queries
const where: Prisma.UserWhereInput = {
email: {
contains: '@example.com',
},
};
```
### Type Query Results
```typescript
// Get inferred type from query
const user = await prisma.user.findUnique({
where: { id: '1' },
include: { posts: true },
});
type UserWithPosts = typeof user;
```
## Common Pitfalls
### 1. N+1 Query Problem
```typescript
// [ERROR] Bad - N+1 queries
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId },
});
}
// [OK] Good - single query with include
const posts = await prisma.post.findMany({
include: { author: true },
});
```
### 2. Not Using Transactions
```typescript
// [ERROR] Bad - can leave inconsistent state
await prisma.user.create({ data: userData });
await prisma.profile.create({ data: profileData }); // If this fails, user exists without profile
// [OK] Good - atomic operation
await prisma.$transaction([
prisma.user.create({ data: userData }),
prisma.profile.create({ data: profileData }),
]);
```
### 3. Exposing Prisma Client to Frontend
```typescript
// [ERROR] Bad - never import Prisma in client components
'use client';
import { prisma } from '@/lib/prisma'; // Security risk!
// [OK] Good - use Server Actions or API routes
'use server';
import { prisma } from '@/lib/prisma';
export async function getUsers() {
return await prisma.user.findMany();
}
```
### 4. Ignoring Soft Deletes
```prisma
model User {
deletedAt DateTime? @map("deleted_at")
}
// Query only non-deleted
const activeUsers = await prisma.user.findMany({
where: { deletedAt: null },
});
```
## Performance Tips
1. **Use indexes** on filtered/sorted fields
2. **Limit result sets** with take/skip
3. **Select only needed fields** to reduce data transfer
4. **Use connection pooling** for serverless environments
5. **Batch operations** instead of loops
6. **Cache frequent queries** at application level
7. **Monitor slow queries** in Supabase dashboard
8. **Use database views** for complex, repeated queries

View File

@@ -0,0 +1,294 @@
# Supabase Integration with Prisma
## Connection Configuration
### Database URLs
Supabase provides two types of connection strings:
**Direct Connection (Port 5432)** - For migrations:
```env
DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
```
**Pooled Connection (Port 6543)** - For queries via pgBouncer:
```env
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
```
**Schema configuration:**
```prisma
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:
```typescript
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:
```typescript
// 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:
```bash
# 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:
```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):
```prisma
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:
```sql
-- 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:
```sql
-- 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:
```bash
npx prisma db pull
```
2. Make changes to schema.prisma
3. Create migration:
```bash
npx prisma migrate dev --name my_changes
```
### Production Deployment
Apply migrations during deployment:
```bash
npx prisma migrate deploy
```
**GitHub Actions example:**
```yaml
- 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
```typescript
// 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
# .env.local
DATABASE_URL="postgresql://postgres:postgres@localhost:54322/postgres"
DIRECT_URL="postgresql://postgres:postgres@localhost:54322/postgres"
```
### Staging/Production
```env
# .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:
```bash
# 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