supabase-nextjs

安装量: 256
排名: #3418

安装

npx skills add https://github.com/alinaqi/claude-bootstrap --skill supabase-nextjs

Supabase + Next.js Skill

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

Core Principle

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 Structure 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

Setup Install Dependencies npm install @supabase/supabase-js @supabase/ssr drizzle-orm postgres npm install -D drizzle-kit

Environment Variables

.env.local

NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=

Server-side only

SUPABASE_SERVICE_ROLE_KEY= DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres

Drizzle Setup drizzle.config.ts 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'], });

src/db/index.ts 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 });

src/db/schema.ts 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('title').notNull(), content: text('content'), published: boolean('published').default(false), createdAt: timestamp('created_at').defaultNow().notNull(), });

// Type exports export type Profile = typeof profiles.$inferSelect; export type NewProfile = typeof profiles.$inferInsert; export type Post = typeof posts.$inferSelect; export type NewPost = typeof posts.$inferInsert;

Supabase Clients src/lib/supabase/client.ts (Browser) import { createBrowserClient } from '@supabase/ssr';

export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); }

src/lib/supabase/server.ts (Server Components/Actions) 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 } }, }, } ); }

src/lib/supabase/middleware.ts (For Middleware) 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 const { data: { user } } = await supabase.auth.getUser();

return { supabaseResponse, user }; }

Middleware src/middleware.ts 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.pathname === '/login' || request.nextUrl.pathname === '/signup')) { return NextResponse.redirect(new URL('/dashboard', request.url)); }

return supabaseResponse; }

export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.\.(?:svg|png|jpg|jpeg|gif|webp)$).)', ], };

Auth Helpers src/lib/auth.ts 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'); } }

Auth Pages src/app/(auth)/login/page.tsx import { requireGuest } from '@/lib/auth'; import { LoginForm } from '@/components/auth/login-form';

export default async function LoginPage() { await requireGuest();

return (

); }

src/components/auth/login-form.tsx '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(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);
  setLoading(false);
  return;
}

router.push('/dashboard');
router.refresh();

};

return (

setEmail(e.target.value)} required />
setPassword(e.target.value)} required />
{error &&

{error}

}
); }

src/app/(auth)/callback/route.ts 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); }

Server Actions src/app/actions/posts.ts '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}); }

export async function updatePost(id: string, formData: FormData) { const user = await requireAuth();

const title = formData.get('title') as string; const content = formData.get('content') as string;

await db.update(posts) .set({ title, content }) .where(eq(posts.id, id));

revalidatePath(/posts/${id}); }

export async function deletePost(id: string) { const user = await requireAuth();

await db.delete(posts).where(eq(posts.id, id));

revalidatePath('/dashboard'); redirect('/dashboard'); }

Data Fetching src/db/queries/posts.ts 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(eq(posts.authorId, userId)) .orderBy(desc(posts.createdAt)); }

export async function getPostById(id: string) { const [post] = await db .select() .from(posts) .where(eq(posts.id, id)) .limit(1);

return post ?? null; }

In Server Components // 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 (

Your Posts

{posts.map((post) => (

{post.title}

{post.content}

))}
); }

Storage Upload Component '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) => { 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) {
  console.error('Upload error:', error);
}

setUploading(false);

};

return ( ); }

Get Public URL 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; }

Realtime Client Component with Subscription '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.filter((p) => p.id !== payload.old.id));
      } else if (payload.eventType === 'UPDATE') {
        setPosts((prev) =>
          prev.map((p) => (p.id === payload.new.id ? payload.new as Post : p))
        );
      }
    }
  )
  .subscribe();

return () => {
  supabase.removeChannel(channel);
};

}, []);

return (

    {posts.map((post) => (
  • {post.title}
  • ))}
); }

OAuth Providers src/components/auth/oauth-buttons.tsx '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 (

); }

Sign Out Server Action // 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'); }

Sign Out Button 'use client';

import { signOut } from '@/app/actions/auth';

export function SignOutButton() { return (

); }

Anti-Patterns Using Supabase client for DB queries - Use Drizzle for type-safety Fetching in client components - Prefer server components Not using middleware for auth - Session refresh is critical Calling cookies() synchronously - Must await in Next.js 15+ Service key in client - Never expose, server-only Missing revalidatePath - Always revalidate after mutations Not handling auth errors - Show user-friendly messages

返回排行榜