用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/artofrawr/claude-control --skill supabase-nextjs命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Process multimedia files with FFmpeg (video/audio encoding, conversion, streaming, filtering, hardware acceleration) and ImageMagick (image manipulation, format conversion, batch processing, effects, composition). Use when converting media formats, encoding videos with specific codecs (H.264, H.265, VP9), resizing/cropping images, extracting audio from video, applying filters and effects, optimizing file sizes, creating streaming manifests (HLS/DASH), generating thumbnails, batch processing images, creating composite images, or implementing media processing pipelines. Supports 100+ formats, hardware acceleration (NVENC, QSV), and complex filtergraphs.
Stripe Checkout, subscriptions, webhooks, customer portal
When the user wants to write, rewrite, or improve marketing copy for any page — including homepage, landing pages, pricing pages, feature pages, about pages, or product pages. Also use when the user says "write copy for," "improve this copy," "rewrite this page," "marketing copy," "headline help," "CTA copy," "value proposition," "tagline," "subheadline," "hero section copy," "above the fold," "this copy is weak," "make this more compelling," or "help me describe my product." Use this whenever someone is working on website text that needs to persuade or convert. For email copy, see email-sequence. For popup copy, see popup-cro. For editing existing copy, see copy-editing.
正在显示 SKILL.md
基于 SOC 职业分类
| name | supabase-nextjs |
| description | Next.js with Supabase and Drizzle ORM |
| disable-model-invocation | false |
Load with: base.md + supabase.md + typescript.md
Next.js App Router patterns with Supabase Auth and Drizzle ORM.
Sources: Supabase Next.js Guide | Drizzle + Supabase
Drizzle for queries, Supabase for auth/storage, server components by default.
Use Drizzle ORM for type-safe database access. Use Supabase client for auth, storage, and realtime. Prefer server components; use client components only when needed.
project/
├── src/
│ ├── app/
│ │ ├── (auth)/
│ │ │ ├── login/page.tsx
│ │ │ ├── signup/page.tsx
│ │ │ └── callback/route.ts
│ │ ├── (dashboard)/
│ │ │ └── page.tsx
│ │ ├── api/
│ │ │ └── [...]/route.ts
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── auth/
│ │ └── ui/
│ ├── db/
│ │ ├── index.ts # Drizzle client
│ │ ├── schema.ts # Schema definitions
│ │ └── queries/ # Query functions
│ ├── lib/
│ │ ├── supabase/
│ │ │ ├── client.ts # Browser client
│ │ │ ├── server.ts # Server client
│ │ │ └── middleware.ts # Auth middleware helper
│ │ └── auth.ts # Auth helpers
│ └── middleware.ts # Next.js middleware
├── supabase/
│ ├── migrations/
│ └── config.toml
├── drizzle.config.ts
└── .env.local
npm install @supabase/supabase-js @supabase/ssr drizzle-orm postgres
npm install -D drizzle-kit
# .env.local
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=<from supabase start>
# Server-side only
SUPABASE_SERVICE_ROLE_KEY=<from supabase start>
DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './supabase/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
schemaFilter: ['public'],
});
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!, {
prepare: false, // Required for Supabase connection pooling
});
export const db = drizzle(client, { schema });
import {
pgTable,
uuid,
text,
timestamp,
boolean,
} from 'drizzle-orm/pg-core';
export const profiles = pgTable('profiles', {
id: uuid('id').primaryKey(), // References auth.users
email: text('email').notNull(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
authorId: uuid('author_id').references(() => profiles.id).notNull(),
title: text().(),
: (),
: ().(),
: ().().(),
});
= profiles.;
= profiles.;
= posts.;
= posts.;
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Called from Server Component - ignore
}
},
},
}
);
}
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
// Refresh session
{ : { user } } = supabase..();
{ supabaseResponse, user };
}
import { type NextRequest, NextResponse } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';
const publicRoutes = ['/', '/login', '/signup', '/auth/callback'];
export async function middleware(request: NextRequest) {
const { supabaseResponse, user } = await updateSession(request);
const isPublicRoute = publicRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
);
// Redirect unauthenticated users to login
if (!user && !isPublicRoute) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('redirectTo', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
// Redirect authenticated users away from auth pages
if (user && (request.nextUrl. === || request.. === )) {
.( (, request.));
}
supabaseResponse;
}
config = {
: [
,
],
};
import { redirect } from 'next/navigation';
import { createClient } from '@/lib/supabase/server';
export async function getUser() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
}
export async function requireAuth() {
const user = await getUser();
if (!user) {
redirect('/login');
}
return user;
}
export async function requireGuest() {
const user = await getUser();
if (user) {
redirect('/dashboard');
}
}
import { requireGuest } from '@/lib/auth';
import { LoginForm } from '@/components/auth/login-form';
export default async function LoginPage() {
await requireGuest();
return (
<div className="flex min-h-screen items-center justify-center">
<LoginForm />
</div>
);
}
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
const supabase = createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
();
;
}
router.();
router.();
};
(
);
}
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/dashboard';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/login?error=auth_error`);
}
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/db';
import { posts, NewPost } from '@/db/schema';
import { requireAuth } from '@/lib/auth';
import { eq } from 'drizzle-orm';
export async function createPost(formData: FormData) {
const user = await requireAuth();
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const [post] = await db.insert(posts).values({
authorId: user.id,
title,
content,
}).returning();
revalidatePath('/dashboard');
redirect(`/posts/${post.id}`);
}
() {
user = ();
title = formData.() ;
content = formData.() ;
db.(posts)
.({ title, content })
.((posts., id));
();
}
() {
user = ();
db.(posts).((posts., id));
();
();
}
import { db } from '@/db';
import { posts, profiles } from '@/db/schema';
import { eq, desc, and } from 'drizzle-orm';
export async function getPublishedPosts(limit = 10) {
return db
.select({
id: posts.id,
title: posts.title,
content: posts.content,
author: profiles.name,
createdAt: posts.createdAt,
})
.from(posts)
.innerJoin(profiles, eq(posts.authorId, profiles.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(limit);
}
export async function getUserPosts(userId: string) {
return db
.select()
.from(posts)
.where((posts., userId))
.((posts.));
}
() {
[post] = db
.()
.(posts)
.((posts., id))
.();
post ?? ;
}
// src/app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth';
import { getUserPosts } from '@/db/queries/posts';
export default async function DashboardPage() {
const user = await requireAuth();
const posts = await getUserPosts(user.id);
return (
<div>
<h1>Your Posts</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
'use client';
import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
export function AvatarUpload({ userId }: { userId: string }) {
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const supabase = createClient();
const fileExt = file.name.split('.').pop();
const filePath = `${userId}/avatar.${fileExt}`;
const { error } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true });
if (error) {
.(, error);
}
();
};
(
);
}
import { createClient } from '@/lib/supabase/server';
export async function getAvatarUrl(userId: string) {
const supabase = await createClient();
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`);
return data.publicUrl;
}
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import { Post } from '@/db/schema';
export function RealtimePosts({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts);
useEffect(() => {
const supabase = createClient();
const channel = supabase
.channel('posts')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts((prev) => [payload.new as Post, ...prev]);
} else if (payload.eventType === 'DELETE') {
setPosts((prev) => prev.( p. !== payload..));
} (payload. === ) {
(
prev.( (p. === payload.. ? payload. : p))
);
}
}
)
.();
{
supabase.(channel);
};
}, []);
(
);
}
'use client';
import { createClient } from '@/lib/supabase/client';
export function OAuthButtons() {
const handleOAuth = async (provider: 'google' | 'github') => {
const supabase = createClient();
await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
};
return (
<div className="space-y-2">
<button onClick={() => handleOAuth('google')}>
Continue with Google
</button>
<button onClick={() => handleOAuth('github')}>
Continue with GitHub
</button>
</div>
);
}
// src/app/actions/auth.ts
'use server';
import { redirect } from 'next/navigation';
import { createClient } from '@/lib/supabase/server';
export async function signOut() {
const supabase = await createClient();
await supabase.auth.signOut();
redirect('/login');
}
'use client';
import { signOut } from '@/app/actions/auth';
export function SignOutButton() {
return (
<form action={signOut}>
<button type="submit">Sign Out</button>
</form>
);
}
cookies() synchronously - Must await in Next.js 15+