Designing system architecture with Cursor's agent
PLArchitecture design with Cursor’s agent is a four-part loop: Plan Mode researches the codebase and compares options before any code is written, a human records the decision as an Architecture Decision Record (ADR), a project rule tells the agent the decision, and CI fitness checks prove every later change still obeys it.
Your SaaS product has just signed its first enterprise customer. The contract asks for tenant data isolation, SSO, role-based access and an audit log, and your current system is one Next.js app on one PostgreSQL database with a users table and no idea of a tenant. The multi-tenancy choice you make this month shapes every query, migration and incident for years, and after the decision the agent will write most of the code that implements it. This page is for the developer or tech lead who owns that decision and does not want to review 50 migrations line by line to know it was carried out.
Covers the Cursor 3.x line; the current release on 2026-10-12 is Cursor 3.24 (2026-10-09, Cursor’s release notes). Cursor’s docs list four agent modes, Agent, Ask, Plan and Debug, and Shift+Tab cycles through them; Cmd+I (Ctrl+I on Windows and Linux) opens the Agent panel. Its run modes are Auto-review, Allowlist and Run Everything, and Cursor names no single default model (all checked 2026-10-12). Plan Mode, Rules and Hooks were first verified against cursor.com on 28 August 2026. dependency-cruiser 18.4.0 was checked with npm view and Postgres MCP Pro 0.3.0 on PyPI on 26 September 2026.
What an architecture session in Cursor leaves behind
Section titled “What an architecture session in Cursor leaves behind”- A constraints file the agent reads first, so its proposals fit your team and budget rather than an imaginary one.
- Four copy-paste prompts: research, option comparison, ADR, and a phased plan in which every phase ships and proves itself.
- An ADR with a “How we verify this holds” section, signed off by a named person.
- A project rule that states the decision, plus the machine checks that enforce it: a dependency rule, two SQL checks on the schema and a tenant-isolation test.
- Recovery steps for the six ways this loop usually fails.
The tool-neutral method behind this page, short ADRs plus machine-checkable constraints, is on architecture decisions agents can follow. Schema-level design, such as keys, indexes and normalisation, belongs to database design in Cursor. This page is the Cursor procedure for one system-level decision from question to enforcement.
How do you run an architecture decision through Cursor?
Section titled “How do you run an architecture decision through Cursor?”The order matters more than any single prompt. Decide before the agent builds, and build the checks before the agent writes the bulk of the code.
- Write the constraints down in
docs/architecture/constraints.md: team size, budget, compliance duties, SLOs and anything already decided. The agent cannot infer that you have three engineers and no on-call rota. - Research in Plan Mode. Plan Mode “creates detailed implementation plans before writing any code” (cursor.com/docs/agent/plan-mode, verified 28 August 2026), so the agent researches and asks questions before it writes any code.
- Compare the options on the dimensions your constraints name, with evidence from the codebase for every claim.
- Decide and record it. A human picks the option. The agent drafts the ADR and you edit it until it says what you mean.
- Encode and enforce. Write the project rule and the fitness checks, and run the checks in CI before the implementation starts.
- Plan the implementation in phases that each deploy on their own and each end with named evidence.
Start each step in a new chat. A fresh context is not anchored on the reasoning of the previous step, and the files on disk carry the state forward.
Step 1: Give the agent your real constraints
Section titled “Step 1: Give the agent your real constraints”A proposal is only as good as the constraints it was optimised for. Without them the agent tends to recommend what suits a large platform team: a database per tenant, a service mesh, an event bus. Write a short file and reference it in every architecture prompt:
# Architecture constraints (owner: Anna Kowalska, reviewed 2026-09-26)
- Team: 3 backend engineers, no dedicated ops; on-call is best effort.- Stack: Next.js App Router, PostgreSQL 17 on a managed service, Drizzle ORM.- Scale today: ~500 users in 20 organisations; target 1,000 tenants in 18 months.- Contract duties: tenant data isolation, SAML 2.0 and OIDC SSO, audit log of all writes, 99.9% monthly uptime.- Budget: one additional managed database at most; no Kubernetes.- Already decided: stay on PostgreSQL; no rewrite of the Next.js app.Step 2: Research the system in Plan Mode
Section titled “Step 2: Research the system in Plan Mode”Switch the agent to Plan Mode and paste the research prompt. The goal of this step is questions, not an answer: the agent should find what you have not told it.
Migration files describe the schema you meant to have. If the agent should also see the one you really have, including constraints and any policies added by hand, connect a read-only database MCP server to a staging copy. Postgres MCP Pro (PyPI postgres-mcp, run with uvx postgres-mcp --access-mode=restricted) limits the agent to read-only transactions. Give it a role that can read the catalog and nothing else, and keep the connection string in your global Cursor MCP configuration, not in the repository. Setup for all three tools is on database MCP servers.
Answer the questions in the chat, then save the inventory and your answers to docs/architecture/multi-tenancy-research.md. Spot-check three of the cited file paths yourself. If one is wrong, the inventory is unreliable, and you should re-run the prompt with a narrower @ scope before you go on.
Step 3: Compare the options with evidence, not adjectives
Section titled “Step 3: Compare the options with evidence, not adjectives”“Row-level security is simpler” is an opinion. “Row-level security changes 38 files, and here they are” is a claim you can check. Ask for the second kind.
Read the table against your constraints, not against the agent’s tone. A useful habit is to fill a small decision table yourself before you read the agent’s recommendation, if you asked for one:
| Criterion (from constraints.md) | Weight | A: RLS | B: schema per tenant | C: database per tenant |
|---|---|---|---|---|
| Isolation meets the contract wording | Must | |||
| Operable by 3 engineers, no ops | Must | |||
| Migration effort | 3 | |||
| Per-tenant restore | 2 | |||
| Cost at 1,000 tenants | 2 |
Run the same prompt for the next decision (SSO: build on a library or buy a provider; what happens to existing password users when their organisation turns SSO on). One decision per chat keeps each comparison focused.
Step 4: Record the decision as an ADR a human signs
Section titled “Step 4: Record the decision as an ADR a human signs”The agent drafts; a named person decides. The ADR is short, and its most important section is the one most templates leave out: how the team will know the decision still holds.
Switch back to Agent mode for this step; it writes one file.
Edit the draft until every line is true, then change the status to “Accepted” in a pull request that the decision owner approves. From then on, the ADR changes only through another pull request; protecting the oracle shows how to keep an agent from editing files like this one.
Step 5: Stop the agent eroding the decision
Section titled “Step 5: Stop the agent eroding the decision”Two layers, and only the second one is a guarantee. A project rule is an instruction the agent reads; a check is something the agent cannot argue with.
Tell the agent: a project rule
Section titled “Tell the agent: a project rule”Create an architecture rule that applies to every session. File placement, scopes and how to confirm a rule is active are on project rules in Cursor. Keep the body short and point to the ADRs rather than restating them:
# Architecture (source of truth: docs/adr/)
- Multi-tenancy (ADR-0003): every tenant-owned table has tenant_id NOT NULL with RLS enabled and forced. New tables need a policy in the same migration. Global tables are listed in the ADR; do not add one without a new ADR.- Data access: route handlers in src/app/api/ call functions in src/repositories/; they never import src/db/ directly.- Every write goes through a repository function that also writes an audit_log row.- Before you finish, run `npm run fitness` and `npm test`, and report their output.- If a task seems to need a change to these rules, stop and say so. Do not work around them.Prove it: fitness checks in CI
Section titled “Prove it: fitness checks in CI”Write the checks now, while the codebase is small and the decision is fresh, and have them fail on the current code where it does not yet comply. Three checks cover this ADR.
Layering, with dependency-cruiser. One rule from the full set on fitness functions:
// .dependency-cruiser.cjs (one rule of the full set)module.exports = { forbidden: [ { name: 'handlers-use-repositories', severity: 'error', comment: 'ADR-0003: route handlers call src/repositories/, never src/db/ directly.', from: { path: '^src/app/api/' }, to: { path: '^src/db/' }, }, ],};Pin typescript@~6.0 as a dev dependency for the analysis. dependency-cruiser 18.4.0 supports only TypeScript below 7.0.0; with TypeScript 7 installed it cruises zero modules and still exits 0. The fitness-functions page has a canary job that fails the build when the check goes blind.
Schema invariants, as SQL that must return zero rows. Save both queries as checks/schema-invariants.sql and run them against a database migrated from scratch in CI:
-- Tenant-owned tables without RLS enabled and forcedSELECT c.relnameFROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespaceWHERE n.nspname = 'public' AND c.relkind = 'r' AND c.relname NOT IN ('plans', 'feature_flags', 'schema_migrations') -- ADR-0003 exceptions AND NOT (c.relrowsecurity AND c.relforcerowsecurity);
-- Tenant-owned tables without a tenant_id columnSELECT t.table_nameFROM information_schema.tables tWHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' AND t.table_name NOT IN ('plans', 'feature_flags', 'schema_migrations') AND NOT EXISTS ( SELECT 1 FROM information_schema.columns c WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name AND c.column_name = 'tenant_id');Behaviour: a tenant-isolation test. The SQL checks prove the policies exist; this test proves they work. Each tenant-owned table gets a policy that compares tenant_id with a per-connection setting. With the second argument true, a connection that never set a tenant sees no rows instead of getting an error:
-- In the migration that creates each tenant-owned table (orders shown)ALTER TABLE orders ENABLE ROW LEVEL SECURITY;ALTER TABLE orders FORCE ROW LEVEL SECURITY;CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant_id', true)::uuid) WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);The test seeds two tenants, connects as app_user with tenant A’s context and asks each tenant-owned table for tenant B’s rows. It also asserts the role and that tenant A sees its own data, so a misconfigured connection or an empty seed cannot turn it green:
import { Client } from 'pg';import { afterAll, beforeAll, expect, it } from 'vitest';
const TENANT_OWNED = ['orders', 'invoices', 'audit_log']; // every table outside the ADR exceptionsconst A = process.env.TENANT_A_ID!; // both tenants seeded before the runconst B = process.env.TENANT_B_ID!;const db = new Client({ connectionString: process.env.APP_DATABASE_URL }); // logs in as app_user
beforeAll(async () => { await db.connect(); await db.query("SELECT set_config('app.tenant_id', $1, false)", [A]);});afterAll(() => db.end());
it('runs as the application role, not the owner', async () => { const { rows } = await db.query('SELECT current_user'); expect(rows[0].current_user).toBe('app_user');});
it('sees its own rows, so the check is not vacuous', async () => { const { rows } = await db.query('SELECT count(*)::int AS n FROM orders'); expect(rows[0].n).toBeGreaterThan(0);});
it.each(TENANT_OWNED)('%s returns no rows of tenant B', async (table) => { const { rows } = await db.query(`SELECT count(*)::int AS n FROM ${table} WHERE tenant_id = $1`, [B]); expect(rows[0].n).toBe(0);});One command for the agent and CI. The project rule and the hook below both call npm run fitness, which runs the layering rule and the schema queries. psql exits 0 when a query returns rows, so the script writes the rows to a file and fails if that file is not empty:
{ "scripts": { "fitness": "depcruise src --config .dependency-cruiser.cjs && psql \"$DATABASE_URL\" -tAq -v ON_ERROR_STOP=1 -f checks/schema-invariants.sql -o .fitness-violations.txt && test ! -s .fitness-violations.txt" }}The isolation test runs under npm test, which the rule also asks for.
Cursor’s hooks run “before or after defined stages of the agent loop and can observe, block, or modify behavior” (cursor.com/docs/hooks, verified 28 August 2026), so you can also run npm run fitness when the agent finishes. Treat that as fast feedback for the agent. CI is the gate, because it runs whether or not a hook is installed.
Step 6: Turn the ADR into phases that each prove themselves
Section titled “Step 6: Turn the ADR into phases that each prove themselves”A big plan fails in the middle, where nothing works and nothing is verified. Ask for phases that each deploy on their own and each end with evidence you can check without reading the diff.
Switch back to Agent mode for this step; it writes one file.
Approve the plan, then run each phase as its own agent task in a new chat that references the plan and the ADR. Checkpoints and branching covers how to back out of a phase that goes wrong locally.
How do you verify the architecture without reading every diff?
Section titled “How do you verify the architecture without reading every diff?”You read the decision and the checks closely once. After that, each pull request is judged on evidence:
| What you need to know | Evidence | Who signs off |
|---|---|---|
| Is the decision the right one? | ADR with options, consequences and verification section | Decision owner, once |
| Does the code still follow the layering? | dependency-cruiser in CI, zero new violations | CI |
| Does every table have isolation in place? | The two SQL checks return zero rows | CI |
| Does isolation actually hold? | Tenant-isolation test as the application role | CI |
| Did a phase meet its acceptance criteria? | The evidence listed for that phase in the plan | Reviewer, against the plan |
| Did anything slip past the checks? | Bugbot or another review agent as an extra reviewer, not a gate | Reviewer reads its findings |
Human review then concentrates on migrations, new policies and changes to the exception list, which is where a wrong line leaks data. How to set up Bugbot and route reviews is on code review in Cursor.
What breaks when you design architecture with Cursor’s agent?
Section titled “What breaks when you design architecture with Cursor’s agent?”The proposal fits a company ten times your size. Symptom: a database per tenant, a message bus and a new service for audit logging, for a team of three. Recovery: check that constraints.md was in the prompt, add “the first thing that breaks for a team of three” as a comparison column, and ask the agent to remove every component your constraints do not require.
The comparison is confident and wrong about your code. Symptom: a cited file does not exist, or the change count is far off. Recovery: spot-check three citations; if one fails, discard the table, narrow the @ scope to the directories that matter, and re-run the comparison prompt in a new chat.
The agent ignores the architecture rule. Symptom: a new route handler imports src/db/ directly. The rule was probably not in context: check the active rules list as described on project rules in Cursor. The dependency-cruiser check fails the build either way; paste its output into the chat and ask for a fix that keeps the rule.
The agent edits the check instead of the code. Symptom: an entry added to the SQL exception list, a rule’s severity lowered to warn, or a new entry in the known-violations baseline. Recovery: revert those files, restart the task, and add the fitness config and the ADR to the paths that need an owner’s approval, as protecting the oracle shows.
The ADR goes stale. Symptom: the rule says RLS, and a later migration moved one domain to a separate database. A stale rule is worse than none because it pushes the agent toward the old design. Recovery: supersede the ADR with a new one in the same pull request as the change, and update the rule and the checks together.
Phase three of five is half-done and nothing works. The plan was written as layers (all migrations, then all queries) instead of deployable slices. Recovery: stop, re-run the plan prompt with the phase rules above, and cut the remaining work so that each phase ends green in CI and in production.