Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
69
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
393
skills/supabase-prisma-database-management/SKILL.md
Normal file
393
skills/supabase-prisma-database-management/SKILL.md
Normal 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
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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`'
|
||||
})
|
||||
@@ -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;
|
||||
158
skills/supabase-prisma-database-management/assets/seed.ts
Normal file
158
skills/supabase-prisma-database-management/assets/seed.ts
Normal 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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user