Next.js + Supabase AI Rules

AI rules for Next.js + Supabase: server-side auth helpers, RLS-first data access, type generation, and storage patterns. Stops the AI from leaking the service-role key into client code.

TypeScriptNext.js + Supabase#nextjs#supabase#postgres#authLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

Next.js + Supabase

Project context

This is a Next.js 15 App Router project that uses Supabase for authentication, Postgres, and (optionally) Storage. Auth and data access run server-side; row-level security (RLS) is the source of truth for authorization. Never leak the service-role key into client code.

Stack

  • Next.js 15 App Router, React 19, TypeScript strict
  • Supabase (Postgres + Auth + Storage)
  • @supabase/supabase-js and @supabase/ssr for SSR-aware clients
  • Tailwind 4 for styling
  • pnpm as the package manager

Environment

NEXT_PUBLIC_SUPABASE_URL=         — public, embedded in client bundle
NEXT_PUBLIC_SUPABASE_ANON_KEY=    — public, embedded in client bundle
SUPABASE_SERVICE_ROLE_KEY=        — SERVER ONLY, never in client code
DATABASE_URL=                     — only if using direct Postgres connection

The service-role key bypasses RLS. It must only ever be used inside 'use server' files or import 'server-only' modules.

Three Supabase clients

Use the right client for the context. Never mix them.

createBrowserClient — used in client components for auth state and realtime subscriptions. Reads the anon key from NEXT_PUBLIC_* env vars.

createServerClient — used in server components, server actions, and route handlers. Reads cookies for auth state.

createAdminClient — service-role client for admin operations (sending emails, modifying users, bypassing RLS). Server-only. Imported only from files marked 'use server' or import 'server-only'.

// lib/supabase/server.ts
import 'server-only'
import { cookies } from 'next/headers'
import { createServerClient as create } from '@supabase/ssr'

export async function createServerClient() {
  const store = await cookies()
  return create(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => store.getAll(),
        setAll: (list) => list.forEach(({ name, value, options }) =>
          store.set(name, value, options)
        ),
      },
    }
  )
}

Auth pattern

  • Sign-in / sign-up flows happen via Server Actions or route handlers, never directly from the browser
  • After sign-in, redirect via redirect() from next/navigation
  • Use middleware to refresh the session on every request
  • Get the current user with await supabase.auth.getUser() — this validates with the auth server, unlike getSession() which only reads the cookie
// In a server component:
const supabase = await createServerClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')

Database access — RLS first

  • Define RLS policies on every table — enable row level security is the default
  • Write the policies in SQL migrations, not the Supabase dashboard (so they're versioned)
  • Test policies with both authenticated and anonymous clients
  • Use the regular client (with RLS) for user-facing queries
  • Use the admin client only for operations the user shouldn't be allowed to do (sending invitation emails, deleting other users' data, etc.)

Type generation

  • Run supabase gen types typescript --linked > types/supabase.ts after every migration
  • Import Database from there and pass to createClient<Database> for type-safe queries
  • Never hand-write Supabase types — always regenerate
import type { Database } from '@/types/supabase'
const supabase = createServerClient<Database>(...)

Storage

  • Use signed URLs for private buckets — never expose direct paths
  • Generate signed URLs server-side; pass them to clients with a short TTL
  • Set bucket policies the same way as RLS — versioned in migrations

Migrations

  • Use supabase migration new <name> to create migrations
  • Never modify a migration after it's been committed — create a new one
  • Run supabase db reset locally before pushing to verify migrations apply cleanly from scratch
  • Keep RLS policies in the same migration as the table they protect

Testing

  • Use a local Supabase instance for tests (supabase start)
  • Reset the DB between tests via supabase db reset or transaction rollback
  • Test RLS by signing in as different users and verifying the right rows are visible/hidden

Patterns to avoid

  • Never embed the service-role key in client code. Search-and-grep for it before every commit.
  • Never call getSession() for authorization. It only reads the cookie. Use getUser() which validates with the server.
  • Never disable RLS. If a query needs to bypass it, use the admin client; don't disable the policy.
  • Don't mix anon and service-role in one file. Pick one per module.
  • Don't query Supabase from useEffect. Fetch in a server component, pass via props.

Tooling

  • pnpm dev — Next.js dev
  • supabase start — local Supabase stack (Postgres, Auth, Storage, Studio)
  • supabase migration new <name> — create a migration
  • supabase db reset — wipe and re-apply migrations locally
  • supabase gen types typescript --linked — regenerate types

AI behavioral rules

  • Always grep for SUPABASE_SERVICE_ROLE_KEY in client-bundled files before committing
  • When fetching user data, prefer RLS-enforced queries from the regular client
  • When asked to add a new table, generate the migration with RLS policies in the same file
  • After creating a migration, regenerate types — never hand-edit types/supabase.ts
  • When asked to add an admin operation, mark the file with 'use server' or import 'server-only' and explain why
  • Never call getSession() for auth gating — always use getUser()
  • Run lint, typecheck, and tests before declaring a task done

Frequently asked

How do I use this Next.js + Supabase rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these Next.js + Supabase rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.

Related stacks