Cursor rules for Supabase + Next.js: a copy-paste supabase.mdc
By VibeBoot ·
Short on time? The SaaS Starter already has this wired up: $49 one-time, 14-day money-back guarantee.
Supabase and Next.js are a common pair for vibe-coded apps, and they are also where AI agents make the most expensive mistakes: trusting an unverified session on the server, shipping a table with Row Level Security off, or putting the service_role key in a NEXT_PUBLIC_ variable. A short, scoped Cursor rule stops most of that. This post gives you one you can paste in today.
Why Supabase needs its own rule
Agents learn from years of examples, and Supabase's Next.js API changed along the way. Left alone, an agent will often:
- import the deprecated
@supabase/auth-helpers-nextjspackage instead of@supabase/ssr; - call
supabase.auth.getSession()in a Server Component and trust it, even though on the server it reads the cookie without verifying it; - cache one server client in a module-level variable, so requests can share state;
- create a table in the dashboard with RLS off and "fix it later";
- reach for the
service_rolekey when a query fails, which bypasses RLS entirely.
None of these break the build. They break security, quietly. That's why they belong in a rule rather than in your memory.
Where the rule goes
Cursor loads project rules from .cursor/rules/*.mdc. A rule with globs is attached automatically when matching files are in context, so the Supabase rule only costs context when you're working on Supabase code:
.cursor/rules/supabase.mdcKeep one short always-on project rule for the stack overview and put Supabase details here.
The supabase.mdc rule
This is the Supabase rule from our free Cursor Rules Pack (MIT), shortened slightly. The full version also covers storage.
---
description: Supabase in Next.js with @supabase/ssr clients, server-side auth checks, Row Level Security, migrations and generated types
globs: **/supabase/**,**/lib/supabase/**,**/utils/supabase/**,**/proxy.ts,**/middleware.ts
alwaysApply: false
---
# Supabase
## Clients
- Use `@supabase/ssr`: `createBrowserClient` in client components, and `createServerClient` with the Next.js `cookies()` store in Server Components, Server Actions and route handlers. Implement only the `getAll` and `setAll` cookie methods. Don't use the deprecated `@supabase/auth-helpers-nextjs`.
- Create a new server client for each request. Never cache one in a module-level variable.
- Refresh the session in `proxy.ts` (Next.js 16) or `middleware.ts` (15) as shown in Supabase's Next.js guide, and return the response object it builds so the refreshed cookies are kept.
- The publishable key (formerly the anon key) may be public via a `NEXT_PUBLIC_` variable. The secret / `service_role` key bypasses RLS: server only, never `NEXT_PUBLIC_`, and only for admin jobs and webhooks.
## Auth checks
- On the server, authorize with `supabase.auth.getClaims()` or `supabase.auth.getUser()`. Never trust `getSession()` on the server.
- Protect pages inside the page, action or route handler. The proxy only refreshes sessions and does optimistic redirects.
## Row Level Security
- Every table in an exposed schema has RLS enabled, with explicit policies per operation.
- Policies compare an owner column to `(select auth.uid())`. Index the columns your policies filter on.
- `update` policies need both `using` and `with check`.
- Don't base authorization on `user_metadata`; users can edit it.
- Even with RLS, filter in code too (`.eq("user_id", user.id)`).
## Schema and types
- Change the schema with migrations in `supabase/migrations/`, not in the dashboard. Never edit a migration that already ran in production.
- Regenerate types after every schema change and pass `Database` to the client generics.
- Always handle `error` from `{ data, error }` before using `data`.Why each line is there
`@supabase/ssr` only. The auth-helpers package is deprecated. Naming the replacement and the two cookie methods (getAll, setAll) stops the agent from mixing old and new examples.
A new server client per request. The server client is bound to the request's cookies. A module-level singleton can leak one user's session into another request.
`getUser()` / `getClaims()` on the server. getSession() reads the session from the cookie, which the browser controls. getUser() asks the Supabase Auth server and getClaims() verifies the JWT, so use one of them for every authorization decision.
The proxy is not your auth check. In Next.js 16 the middleware file is proxy.ts. It's good for refreshing sessions and redirecting signed-out users early, but the real check belongs next to the data access, in the page, Server Action or route handler.
`service_role` never behind `NEXT_PUBLIC_`. Anything with that prefix is inlined into the browser bundle, and the service_role key bypasses every RLS policy. Agents sometimes add the prefix just to silence an "undefined" error. Not sure your .env is clean? Paste it into the free env leak checker: it flags a service_role key, database URL or Stripe secret behind a public prefix, right in your browser.
RLS on, always. With the publishable key in the browser, RLS is what keeps users out of each other's rows. (select auth.uid()) in a policy lets Postgres evaluate it once per query instead of per row, and with check on updates stops a user from reassigning a row to someone else.
Migrations and generated types. Dashboard edits aren't reviewable and drift between environments. Generated types turn a renamed column into a type error the agent has to fix.
Pair it with AGENTS.md
Cursor rules are Cursor-specific. If you also use Codex, Claude Code or another agent, put the essentials in AGENTS.md in your repo root:
## Auth and data access
- Use createServerClient (server) and createBrowserClient (client) from @supabase/ssr. New server client per request.
- On the server, authorize with supabase.auth.getUser() or getClaims(). Never trust getSession() on the server.
- Every table in an exposed schema has RLS enabled. The service_role key is server-only.You don't have to write that by hand: the free AGENTS.md generator builds a full file for Next.js + Supabase (plus Stripe, Tailwind, shadcn/ui or the AI SDK if you use them), and adds a one-line CLAUDE.md that imports it.
Test the rule
Open a new Cursor chat with a file under lib/supabase/ in context and ask: "Add a page that lists the current user's projects." A good result creates a server client in the page, calls getUser() (or getClaims()), redirects when there's no user, filters by user_id, and asks you to add an RLS policy if the table doesn't have one. If it reaches for getSession() or the service_role key, check that the rule shows up as attached in the chat.
Get the rest of the rules
The Cursor Rules Pack is free and MIT-licensed: scoped rules for Next.js, Supabase, Stripe, Tailwind + shadcn/ui, the AI SDK, security and workflow, plus an AGENTS.md template. Download the zip, copy it into your project and trim what you don't use.
If you'd rather start from a working app, the VibeBoot SaaS Starter ships Auth.js, Stripe subscriptions and Prisma with matching Cursor rules, AGENTS.md and CLAUDE.md. It uses Prisma rather than the Supabase client, and its database can run on Supabase Postgres by switching the Prisma provider to Postgres and setting your connection string.