← Back to Guides · CURSOR RULES
Cursor Rules for SaaS Founders: A Production Setup That Agents Actually Follow
You spent an evening writing a rules file. The next morning Cursor shipped an API route that accepts raw req.json(), never checks a session, and returns { error: err.message } with a stack trace.
The file is still in the repo. The agent did not ignore you out of spite. It never loaded the file, or it loaded 800 lines of style-guide noise and lost the one sentence that mattered.
This is the setup I use for production SaaS in Cursor: a small always-on core, glob-scoped rules for API / database / UI / tests, and a habit of turning repeated agent mistakes into eight-line rules instead of longer lectures.
The takeaway
Four focused .mdc files beat one manifesto.
- Always-on core: under ~80 lines. Project law only: read
CURSOR.md, error shape, tenant-from-session, what not to touch. - Glob-scoped specialists: API rules fire on
app/api/**, database rules ondb/**anddrizzle/**, UI rules onapp/**/*.tsx. - Write rules like code-review comments. A good rule is specific, has a good/bad example, and names the file pattern it applies to.
- If a linter can catch it, do not put it in a rule. Rules spend tokens on every matching turn. ESLint is cheaper and more reliable.
Cursor's own docs say the same thing in different words: keep rules focused, under 500 lines, split them, and avoid dumping a style guide. The rest of this article is how that advice looks when the product is a multi-tenant SaaS, not a todo app.
What Cursor rules actually are
Large language models do not remember yesterday's chat. Rules are prompt-level context that get prepended when they apply. That is the whole mechanism. There is no hidden memory, no compiler, no enforcement beyond "this text was in the prompt."
Cursor currently has four places to put that text:
| Kind | Where | Use for |
|---|---|---|
| Project rules | .cursor/rules/*.mdc | Version-controlled, scoped with frontmatter |
| User rules | Cursor Settings → Rules | Your personal voice across every repo |
| Team rules | Cursor dashboard (Team/Enterprise) | Org-wide policy; they win when guidance conflicts |
AGENTS.md | Repo root or a subdirectory | Plain markdown, no frontmatter, good as a portable floor |
Project rules must be .mdc files. A plain .md sitting in .cursor/rules is ignored because it has no frontmatter. That surprise shows up in the Cursor forum constantly.
A root .cursorrules file is the older single-file format. If your agent is ignoring a giant .cursorrules, treat that as a migration signal, not a model failure. Move the useful parts into scoped .mdc files.
Rules also do not apply to Cursor Tab, and User rules do not apply to inline edit (Cmd/Ctrl+K). If you expected Tab to honor your Zod rule, that is why it didn't.
The four activation modes
Frontmatter decides whether a project rule is even in the prompt:
alwaysApply | description | globs | When it loads |
|---|---|---|---|
true | ignored | ignored | Every Agent chat |
false | (none) | set | When a matching file is in context |
false | set | omitted | When the agent thinks the description is relevant |
false | omitted | omitted | Only when you @-mention the rule |
This table is the difference between "my rules work" and "Cursor ignores me." A 600-line alwaysApply: true file is a context tax. A precise glob is a scalpel.
Official precedence when sources conflict: Team → Project → User. All applicable rules are merged; earlier sources win.
The SaaS rule stack
A founder shipping Next.js + Postgres + Stripe does not need a rule for every library. You need the agent to stop making the same five production mistakes.
This is the tree I install:
.cursor/rules/
core.mdc # alwaysApply: true
api-routes.mdc # globs: **/app/api/**/*.ts
database.mdc # globs: **/db/**,**/drizzle/**,**/prisma/**
react-ui.mdc # globs: **/app/**/*.tsx
tests.mdc # globs: **/*.test.ts,**/*.spec.ts
payments.mdc # description only, no globs. Agent pulls it in for Stripe
In AgenticKit these files ship as shipkit-core, api-routes, database, react-ui, tests, plus marketing rules in the other kit. You can copy the pattern without the product. The names are not magic. The split is.
What goes in the always-on core
Only things that are true on every turn:
- Read
CURSOR.mdbefore changing code. That file is where the stack, scripts, tenant field, and "what not to do" live. - Copy the nearest existing file. Do not invent a new architecture because the training data prefers it.
- The API error shape. In our kit it is required, not suggested:
// required
{ error: string, code: string }
- Tenant identity comes from the session, never from the request body alone.
- End the turn with: files changed, how to test, what's next.
Here is a shortened core file you can paste. It is modeled on shipkit-core.mdc in the kit. The real file is a bit longer because it also wires slash-command role adoption.
---
description: Project core. Read CURSOR.md first. Match repo patterns.
alwaysApply: true
---
# Core
Before any code change:
1. Read `CURSOR.md` for stack, paths, scripts, skip flags, and what NOT to do
2. Copy the nearest existing file. Do not invent a new architecture
## Errors
Every API error returns:
{ "error": "Human-readable message", "code": "MACHINE_READABLE_CODE" }
`code` is required. Clients and tests depend on stable codes.
## Multi-tenant (if CURSOR.md says yes)
Filter every query by tenant from the session. Never trust `organizationId` from the client body.
## End every task with
Files changed · how to test · what's next
That is enough always-on text. Resist the urge to add TypeScript style, React patterns, and Stripe trivia here. Those belong in glob files.
API routes: the rule that pays rent
SaaS dies in handlers that skip validation or auth. The API rule should fire only when the agent is actually in app/api.
---
description: API route standards. Zod, auth, typed errors, thin handlers
globs: "**/app/api/**/*.ts"
alwaysApply: false
---
# API routes
Handler anatomy: auth → validate (Zod) → service call → map response.
Route files stay thin. Business logic lives in `src/lib/`.
Validate params, query, and body with Zod before any database call.
1. Resolve session first. Missing session → 401 `{ error, code: "UNAUTHORIZED" }`
2. Filter every query by tenant from the session. Cross-tenant → 403 or 404
3. Error shape is `{ error: string, code: string }`. Never return a stack trace.
Webhooks: read the raw body before JSON parse, verify the signature, store `event.id` before processing, return 200 quickly.
A good/bad pair in the rule is worth more than another paragraph of policy. This is the example I keep in ours:
// good: tenant from session
const rows = await db.select().from(items).where(
and(eq(items.organizationId, session.organizationId), eq(items.id, id))
)
// bad: client supplies the org
const { organizationId } = await req.json()
And the error envelope, because models love returning { error: "Not found" } with no machine-readable code:
// good
return NextResponse.json(
{ error: "Invoice not found", code: "INVOICE_NOT_FOUND" },
{ status: 404 }
)
// bad: missing code, or worse, a leaked stack
return NextResponse.json({ error: "Not found" }, { status: 404 })
If you want the longer version we ship, including the status-code table, it lives in the API patterns skill the api-engineer agent is told to read. That is a skill, not a rule: see Cursor rules vs skills and the Cursor skills pillar.
Database: tenant-leading indexes and boring migrations
Database rules should not teach SQL. They should stop the two SaaS foot-guns: indexes that ignore the tenant, and migrations with no down path.
---
description: Schema, tenant indexes, safe migrations
globs: "**/db/**,**/drizzle/**,**/prisma/**"
alwaysApply: false
---
# Database
Composite indexes lead with the tenant column.
-- good
CREATE INDEX idx_items_tenant_created ON items (organization_id, created_at DESC);
-- bad: sequential scan per tenant as the table grows
CREATE INDEX idx_items_created ON items (created_at DESC);
Every migration ships with up SQL, down SQL, a risk of LOW / MED / HIGH, and a backfill plan if data is rewritten.
Timestamps are `timestamptz`. Foreign keys declare ON DELETE. Soft deletes use a partial index WHERE deleted_at IS NULL.
This is the entire useful core of our database rule. The rest is "read the Drizzle/Prisma skill before you generate a migration," which is a pointer, not a pasted manual.
UI and tests: states, not aesthetics
The UI rule I actually want is not "use Tailwind." The agent already knows Tailwind. What it forgets is the SaaS surface area of a route:
- Server Components by default;
"use client"only at the leaf that needs events or hooks - Every user-facing segment gets
loading.tsx,error.tsx, and an empty state - Forms have labels. Buttons that are not submit buttons have
type="button"
The test rule is similarly narrow. For each API feature, require four cases: happy path, validation 400, auth 401, and tenant isolation. A test named it('works') is a failed review, not a test.
Name the isolation case the way you want to read a failure later:
it('should return 404 when invoice belongs to another tenant', async () => {
const res = await GET(invoiceId, { session: tenantBSession })
expect(res.status).toBe(404)
})
Payments: intelligent, not always-on
Stripe does not belong in the always-on file. Most turns are not about billing. Give payments.mdc a description and no globs:
---
description: Stripe subscriptions, checkout, webhooks, customer portal. Use when the task mentions billing, invoices, plans, or Stripe.
alwaysApply: false
---
# Payments
- Verify signatures with the raw body
- Idempotency: persist `event.id` before side effects
- Handle `customer.subscription.updated` and `invoice.payment_failed` explicitly
- Customer Portal for plan changes and cancellation. Do not invent a settings page that writes subscription status by hand
The agent pulls this in when the description matches. That is what "Apply Intelligently" is for.
Rules vs AGENTS.md vs CURSOR.md vs skills
These four files get mashed together in blog posts. They are not the same tool.
| File | Job | Loaded how | Put this here |
|---|---|---|---|
.cursor/rules/*.mdc | Constraints the agent must not violate | Frontmatter | Error shape, tenant rule, glob-scoped patterns |
AGENTS.md | Portable "README for agents" | Directory scope | Setup commands, how to run tests, PR title format |
CURSOR.md | Live project truth | You tell the always-on rule to read it | Stack versions, script names, skip flags, current priorities |
Skills (SKILL.md) | Multi-step playbooks | On demand via /skill or @skill | How to add a Stripe webhook, end to end |
Cursor's agent best-practices post draws the same line: rules are static context; skills are dynamic workflows. Official docs also say to prefer a skill when the instructions are a procedure, and a rule when a short constraint is enough.
A practical split I use:
- "Zod on every input" → rule
- "Here is the 12-step Stripe webhook procedure" → skill
- "We use Drizzle, not Prisma, and
pnpm testnotnpm test" →CURSOR.md - "Clone,
pnpm i,pnpm test, PR title format" →AGENTS.mdso Codex / Claude Code / Cursor all see a floor
If you keep repeating a prompt, Cursor's own advice is to turn it into a command or skill, not a bigger always-on rule.
CURSOR.md is the file I care about most after the core rule. It is where the human writes the stack and the skip flags (Skip DB, Skip UI, Skip E2E). Rules stay stable. CURSOR.md changes when the product changes. I will write the memory-bank setup separately; the only thing that matters here is: the always-on rule must tell the agent to read it.
Why your rules get ignored
This is the list I hear from founders, and the one I hit while writing AgenticKit's own rules.
1. The file is the wrong format. .md in .cursor/rules does nothing. alwaysApply is missing. A glob does not match the files in context (src/app/api vs app/api).
2. "Apply Intelligently" has no description. Without a description the agent cannot decide relevance. You get a manual rule you never @-mention.
3. Always-on bloat. A 600-line core file competes with the actual task. The model keeps the nearby code and drops your tenth "never use any" bullet. Cursor's 500-line guidance exists because this happens.
4. Conflicts. User rule says "be concise and just write the code." Project rule says "read three skills and produce a spec first." Team rule says something else. Precedence is Team → Project → User, but three contradictory floors still produce mush. Delete the extras.
5. You put a one-off product fact in a rule. "The login page should have a forgot-password link" is a ticket, not a rule. A good Reddit rule of thumb: "Every route must validate input and return errors" is a rule. A specific screen's copy is not.
6. You expected Tab or Cmd+K to obey project rules. They don't. Agent chat does.
When you are debugging this, do not add more text. Open the rule, check the frontmatter, check the glob against the file the agent is editing, and look at what Cursor shows as applied in the rule UI.
The feedback loop that makes rules get better
Do not design a perfect stack on day one. Cursor's docs are explicit: start simple, add a rule when the agent repeats a mistake.
The loop I actually run:
- The agent ships something wrong. Example: a list endpoint that takes
organizationIdfrom the query string. - I revert or fix the code myself if the blast radius is small.
- I add or tighten one rule, usually an eight-line good/bad example in
api-routes.mdc. - I start a new chat and give the same task. Old chats are already poisoned with the bad pattern.
- If the same mistake happens in a third chat, the rule is still vague. I add a concrete snippet, not another adjective.
That is how the required code field got into our core rule. Models happily return { error: "Not found" }. Tests and clients cannot branch on a string that changes with the writer's mood. After enough broken fixtures, the rule became "code is required," with a good and a bad snippet, not a paragraph about "consistent error handling."
Treat every agent mistake as a candidate rule. Most will not qualify. The ones that do are the ones you have now corrected twice.
What does not belong in rules
- An entire style guide. Use ESLint, Prettier, and
tsc --noEmit. The agent already knows common TypeScript style. Official Cursor guidance is to point at a canonical file instead of pasting a guide. - Every CLI command you might run. The agent knows
pnpm,git, andpytest. Put the project-specific scripts inCURSOR.md(pnpm test, not a guessednpm test). - Rare edge cases. A rule that applies on 2% of turns is a tax on the other 98%.
- Duplicated source code. Reference
@src/lib/errors.tsinstead of pasting it. Pasted code goes stale; a pointer does not. - Marketing voice, commit-message poetry, or "be world-class." Those words do not change a single AST node.
If you already have a bloated .cursorrules, drop it on the Rules File Grader and look at what is actually testable. Then convert the keepers with the Config Converter rather than rewriting from a blank page.
When this stack is the wrong tool
Do not install six SaaS rules on a throwaway spike. A single AGENTS.md that says "TypeScript, no tests, delete this folder on Friday" is enough.
Skip glob-scoped API rules if you are not building an API. Skip tenant rules if you do not have tenants. A hardcoded organizationId filter in a single-user app is cargo cult, not safety.
Do not use always-on rules as a substitute for a failing test. A rule that says "never leak tenants" plus no isolation test is a wish. The rule tells the agent what to write; the test tells you whether it did.
And do not use this stack to paper over a missing product decision. "Add billing" is not a rules problem. It is a spec problem. Write down whether you mean Checkout, Customer Portal, usage meters, or a single lifetime SKU before you ask the agent to invent a webhook.
A one-hour setup
If you do nothing else this week:
- Create
.cursor/rules/core.mdcwithalwaysApply: trueand the short core above. Point it at a filled-inCURSOR.md(stack, scripts, tenant field, what not to do). - Add
api-routes.mdcif you haveapp/api. Adddatabase.mdcif you have a schema folder. Stop there. - Delete or archive any root
.cursorruleslonger than a page, after moving the two or three sentences that are still true. - The next time the agent repeats a mistake, add a good/bad snippet to the matching glob file and start a new chat.
That is a production rules setup. Not a prompt museum.
The tech-lead agent in AgenticKit exists to keep this floor intact. It is explicitly not allowed to write production code, which is how the rules stay rules instead of becoming more generated TypeScript. If you want the same stack without writing it, npx agentickit-cursor init drops these files into .cursor/rules. If you want to stay independent, copy the four files in this article and fill CURSOR.md yourself. Either way, grade what you have before you add more text.
Next: the shipping workflow that these rules are meant to hold, and the CURSOR.md memory setup the always-on rule is required to read.
Related Tools & Agents
Grade the file you have
Drop a .mdc or .cursorrules file into the Rules Grader, then add one SaaS-specific rule this week.
Open the Rules File Grader