Installs a 3-layer Cursor-aligned agent pipeline so future agent
sessions can orient quickly and stay inside guard rails:
L1 — context for any agent reading the repo:
- AGENTS.md (top-level orientation, conventions, no-go zones)
- .cursor/rules/ (no-go-zones, api-routes, prisma, prisma-schema-map)
- .cursor/skills/ (add-api-route, add-prisma-model)
- docs/SCHEMA_MAP.md generated from prisma/schema.prisma
- scripts/generate-schema-map.ts (regenerate the map; wired up as
`npm run schema:map`)
L2 — subagent roles for the 9-stage idea-to-feature pipeline:
- .cursor/agents/role-*.md (conductor, architect, ia-architect,
design-system-auditor, implementer, reviewer, ux-reviewer,
a11y-auditor, doc-writer) with explicit multitask annotations.
L3 — pipeline scaffolding:
- .github/CODEOWNERS, PR template, and CI workflows (ci.yml,
preview-smoke.yml, visual-diff.yml, pr-health-rollup.yml).
Test job is intentionally disabled until Playwright is wired up.
- .convoys/ folder for per-feature run notes + scripts/log-convoy-event.sh.
- scripts/wt.sh worktree helper.
- src/lib/flags/index.ts simple env-driven feature flag wrapper.
- tests/smoke/app.smoke.spec.ts (Playwright smoke; excluded from
tsc until @playwright/test is installed — see tsconfig change).
Also writes .agent-context-manifest.yml so the sync-agent-context
skill can detect drift and offer selective updates from upstream.
Follow-ups (not in this commit):
- Install @playwright/test and re-enable the test job in ci.yml.
- Review .cursor/agents/role-*.md and trim any roles that don't
apply to this codebase.
Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.8 KiB
Text
98 lines
3.8 KiB
Text
---
|
|
description: Conventions for Next.js App Router API route handlers in this repo
|
|
globs: src/app/api/**/route.ts
|
|
---
|
|
|
|
# API Route Conventions
|
|
|
|
Every `src/app/api/**/route.ts` follows the same shape: auth → parse body → query Prisma scoped to `orgId` → return JSON, all wrapped in `try / handleApiError`.
|
|
|
|
## Authentication & Authorization
|
|
|
|
- Import from `@/lib/api-auth`:
|
|
|
|
```ts
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { requireApiAuthWithOrg, handleApiError } from "@/lib/api-auth";
|
|
import { prisma } from "@/lib/db";
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const session = await requireApiAuthWithOrg();
|
|
// session.user.id, session.user.orgId, session.user.role
|
|
// ...
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
}
|
|
```
|
|
|
|
- `requireApiAuthWithOrg()` returns an `OrgSession` (extends NextAuth `Session` with `user.id` and `user.orgId`) — both are always defined after this call.
|
|
- Pass an optional `Action` to enforce a permission in one line: `await requireApiAuthWithOrg("cards.delete")`. Throws `PermissionError`; `handleApiError` returns 403.
|
|
- For routes that don't need org scoping (rare — typically auth callbacks), use `requireApiAuth()` instead.
|
|
|
|
## Multi-tenancy is mandatory
|
|
|
|
Every Prisma query against an org-scoped model (`ResponseCard`, `FormTemplate`, `Person`, `Integration`, …) MUST scope by `organizationId`:
|
|
|
|
```ts
|
|
const card = await prisma.responseCard.findUnique({ where: { id } });
|
|
if (!card || card.organizationId !== session.user.orgId) {
|
|
return NextResponse.json({ error: "Card not found" }, { status: 404 });
|
|
}
|
|
```
|
|
|
|
For lists: include `organizationId: session.user.orgId` in the `where` filter directly. Returning a 404 (not 403) on cross-org access is the convention so we don't leak existence.
|
|
|
|
## Request validation
|
|
|
|
- Body parsing is hand-rolled today. Use the defensive pattern from `src/app/api/cards/[id]/route.ts`:
|
|
|
|
```ts
|
|
const body = await request.json().catch(() => ({}));
|
|
const data: Record<string, unknown> = {};
|
|
const stringFields = ["name", "email", /* ... */];
|
|
for (const field of stringFields) {
|
|
if (body[field] != null) data[field] = String(body[field]);
|
|
}
|
|
```
|
|
|
|
- Zod is in `package.json` but not widely used. If you reach for it in a new route, that's fine — just stay consistent within the route.
|
|
|
|
## Error handling
|
|
|
|
- Wrap every handler in `try { ... } catch (error) { return handleApiError(error); }`. Never throw to the framework.
|
|
- `handleApiError` returns 401 for `ApiAuthError`, 403 for `PermissionError`, 500 (with `console.error`) for anything else.
|
|
- For domain errors that aren't auth/permission, return `NextResponse.json({ error: "..." }, { status: 4xx })` directly — don't invent new error classes for one-off cases.
|
|
|
|
## Database access
|
|
|
|
- Always `import { prisma } from "@/lib/db";` — the lazy `Proxy` singleton. Never `new PrismaClient()`.
|
|
- Use `select` or `include` only when you need it; the default fetch is fine for small models.
|
|
- For writes, prefer `update`/`create` over `upsert` unless you actually need both paths.
|
|
|
|
## Response shape
|
|
|
|
- Success collections: `NextResponse.json({ items, total, page, limit })` (see `src/app/api/cards/route.ts` GET).
|
|
- Success single: `NextResponse.json(record)` (no envelope).
|
|
- Created: `NextResponse.json(record, { status: 201 })`.
|
|
- Errors: `{ error: string, action?: string }` — `handleApiError` already does this.
|
|
|
|
## Dynamic routes
|
|
|
|
Next.js 16 dynamic route params are async. Use:
|
|
|
|
```ts
|
|
export async function GET(
|
|
_request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const { id } = await params;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## After adding / changing a route
|
|
|
|
- Update the route table in `README.md` if it's a public-shape change.
|
|
- If the route writes to `ResponseCard.firstName` or `lastName`, recompute `name` (see the canonical recompute block in `src/app/api/cards/[id]/route.ts`).
|