Cursor · Supabase · Stripe ·
Cursor Rules for Next.js + Supabase + Stripe: A Security-Focused Starting Set
AI agents write Next.js, Supabase and Stripe code fast, but they make the same handful of mistakes, and on this stack those mistakes are security bugs. This post gives you a working set of rules for Cursor (.cursor/rules/*.mdc) and any agent that reads AGENTS.md, and explains what each one prevents.
This guide pairs with Ship-Ready AI Rules, and if you want the next step, read Supabase Service Role Key Exposed? How to Check, Rotate and Stop It Happening Again.
How Cursor rules work (quick recap)
Project rules live in .cursor/rules/ as .mdc files with front matter: a description, optional globs (which files trigger the rule), and alwaysApply. Keep "always" rules short and put detail in glob-scoped rules so they load only when relevant. Many agents (Claude Code, Codex and others) also read AGENTS.md at the repo root, so put your core rules there and point Cursor at it.
Rule 1: Two Supabase clients, never mixed
The most common, and most dangerous, agent "fix" is swapping in the service/secret key when a query returns nothing. That bypasses Row Level Security for everyone. If it has already leaked, follow how to check for and rotate an exposed service role key.
---
description: Supabase clients
globs: app/**/*.ts,app/**/*.tsx,lib/supabase/**
alwaysApply: false
---
- Acting as a user: await createUserClient() (publishable key, RLS applies).
- Admin client (secret key) ONLY in webhooks, cron and trusted jobs. Never in
components or client code. If a query returns nothing, fix the RLS policy.
Rule 2: RLS in the same migration as the table
create table public.notes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
body text not null
);
create index on public.notes (user_id);
alter table public.notes enable row level security;
create policy "own notes" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
Wrapping auth.uid() in select lets Postgres evaluate it once per query rather than per row.
Rule 3: Authorise with getUser() on the server
Don't authorise with getSession() (cookie data that isn't re-validated) or with a userId sent by the browser.
Rule 4: Stripe webhooks, raw body first
export const runtime = "nodejs";
export async function POST(req: Request) {
const body = await req.text(); // NOT req.json()
const sig = req.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET);
// insert event.id into stripe_events; on unique violation return 200 (already processed)
}
Rule 5: Grant access from the webhook, not the success URL
Anyone can visit /success. Mirror subscription state from customer.subscription.* events into a table and gate features on that row (active / trialing).
Rule 6: Next.js 16 specifics
cookies(),headers(),paramsandsearchParamsare async:awaitthem.- Session refresh lives in
proxy.ts(the replacement formiddleware.ts). Exclude your webhook path from its matcher. - Modules that touch secrets start with
import "server-only".
Rule 7: Safe redirects
Auth flows love ?next=/somewhere. Only allow relative paths starting with a single /, otherwise you've built an open redirect.
A definition of done
End every rule set with a checklist the agent must satisfy: typecheck, tests and build pass; new tables have RLS; no secrets in client bundles; webhooks idempotent.
.mdc rule are on GitHub (MIT). Full pack: Ship-Ready AI Rules v2 ($19) adds 3 guardrail hooks, 18 Claude Code skills, 6 subagents, all the Cursor rules and a demo app with tests.