Cursor Rules for SaaS Founders: A Production Setup That Agents Actually Follow

By Rakshit Yadav (@yadavrakshit60)•Aug 2026•14 min read

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 on db/** and drizzle/**, UI rules on app/**/*.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:

KindWhereUse for
Project rules.cursor/rules/*.mdcVersion-controlled, scoped with frontmatter
User rulesCursor Settings → RulesYour personal voice across every repo
Team rulesCursor dashboard (Team/Enterprise)Org-wide policy; they win when guidance conflicts
AGENTS.mdRepo root or a subdirectoryPlain 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:

alwaysApplydescriptionglobsWhen it loads
trueignoredignoredEvery Agent chat
false(none)setWhen a matching file is in context
falsesetomittedWhen the agent thinks the description is relevant
falseomittedomittedOnly 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:

  1. Read CURSOR.md before changing code. That file is where the stack, scripts, tenant field, and "what not to do" live.
  2. Copy the nearest existing file. Do not invent a new architecture because the training data prefers it.
  3. The API error shape. In our kit it is required, not suggested:
// required
{ error: string, code: string }
  1. Tenant identity comes from the session, never from the request body alone.
  2. 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.

FileJobLoaded howPut this here
.cursor/rules/*.mdcConstraints the agent must not violateFrontmatterError shape, tenant rule, glob-scoped patterns
AGENTS.mdPortable "README for agents"Directory scopeSetup commands, how to run tests, PR title format
CURSOR.mdLive project truthYou tell the always-on rule to read itStack versions, script names, skip flags, current priorities
Skills (SKILL.md)Multi-step playbooksOn demand via /skill or @skillHow 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 test not npm test" → CURSOR.md
  • "Clone, pnpm i, pnpm test, PR title format" → AGENTS.md so 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:

  1. The agent ships something wrong. Example: a list endpoint that takes organizationId from the query string.
  2. I revert or fix the code myself if the blast radius is small.
  3. I add or tighten one rule, usually an eight-line good/bad example in api-routes.mdc.
  4. I start a new chat and give the same task. Old chats are already poisoned with the bad pattern.
  5. 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, and pytest. Put the project-specific scripts in CURSOR.md (pnpm test, not a guessed npm 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.ts instead 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:

  1. Create .cursor/rules/core.mdc with alwaysApply: true and the short core above. Point it at a filled-in CURSOR.md (stack, scripts, tenant field, what not to do).
  2. Add api-routes.mdc if you have app/api. Add database.mdc if you have a schema folder. Stop there.
  3. Delete or archive any root .cursorrules longer than a page, after moving the two or three sentences that are still true.
  4. 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

🛠️ Free Tool: rules-grader🛠️ Free Tool: config-converter🤖 Agent: tech-lead🤖 Agent: api-engineer🤖 Agent: security-auditor⚡ Command: /audit⚡ Command: /shipSkill: project-conventionsSkill: api-patternsSkill: error-handling

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