Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    alinaqi

    supabase-nextjs

    alinaqi/supabase-nextjs
    Coding
    467
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Next.js with Supabase and Drizzle ORM

    SKILL.md

    Supabase + Next.js Skill

    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=<from supabase start>
    
    # Server-side only
    SUPABASE_SERVICE_ROLE_KEY=<from supabase start>
    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 (
        <div className="flex min-h-screen items-center justify-center">
          <LoginForm />
        </div>
      );
    }
    

    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<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);
          setLoading(false);
          return;
        }
    
        router.push('/dashboard');
        router.refresh();
      };
    
      return (
        <form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
          <div>
            <label htmlFor="email">Email</label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div>
            <label htmlFor="password">Password</label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          {error && <p className="text-red-500">{error}</p>}
          <button type="submit" disabled={loading}>
            {loading ? 'Signing in...' : 'Sign In'}
          </button>
        </form>
      );
    }
    

    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 (
        <div>
          <h1>Your Posts</h1>
          {posts.map((post) => (
            <article key={post.id}>
              <h2>{post.title}</h2>
              <p>{post.content}</p>
            </article>
          ))}
        </div>
      );
    }
    

    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<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) {
          console.error('Upload error:', error);
        }
    
        setUploading(false);
      };
    
      return (
        <input
          type="file"
          accept="image/*"
          onChange={handleUpload}
          disabled={uploading}
        />
      );
    }
    

    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 (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    }
    

    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 (
        <div className="space-y-2">
          <button onClick={() => handleOAuth('google')}>
            Continue with Google
          </button>
          <button onClick={() => handleOAuth('github')}>
            Continue with GitHub
          </button>
        </div>
      );
    }
    

    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 (
        <form action={signOut}>
          <button type="submit">Sign Out</button>
        </form>
      );
    }
    

    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
    Recommended Servers
    Prisma
    Prisma
    EasyWeek
    EasyWeek
    Neon
    Neon
    Repository
    alinaqi/claude-bootstrap
    Files