# Project Rules ## Tooling - **Package manager**: `bun` (never npm/yarn/pnpm) - **Build**: `bun run build` (uses turbo). Filter: `bun run --filter '@trycompai/app' build` - **Typecheck**: `bun run typecheck` or `bunx turbo run typecheck --filter=@trycompai/api` - **Tests (app)**: `cd apps/app && bunx vitest run` - **Tests (api)**: `cd apps/api && bunx jest src/ --passWithNoTests` - **Lint**: `bun run lint` ## Code Style - **Max 300 lines per file.** Split into focused modules if exceeded. - **No `as any` casts.** Ever. Use proper types, generics, or `unknown` with type guards. - **No `@ts-ignore` or `@ts-expect-error`.** Fix the type instead. - **Strict TypeScript**: Use zod for runtime validation, generics over `any`. - **Early returns** to avoid nested conditionals. - **Named parameters** for functions with 2+ arguments. - **Event handlers**: prefix with `handle` (e.g., `handleSubmit`). ## Monorepo Structure ``` apps/ api/ # NestJS API (auth, RBAC, business logic) app/ # Next.js frontend (compliance + security products) portal/ # Employee portal packages/ auth/ # RBAC definitions (permissions.ts) — single source of truth db/ # Prisma schema + client ui/ # Legacy component library (being phased out) ``` ## Authentication & Session - **Auth lives in `apps/api` (NestJS).** The API is the single source of truth for authentication via better-auth. All apps and packages that need to authenticate (app, portal, device-agent, etc.) MUST go through the API — never run a local better-auth instance or handle auth directly in a frontend app. - **Session-based auth only.** No JWT tokens. Cross-subdomain cookies (`.trycomp.ai`) allow sessions to work across all apps. - **HybridAuthGuard** supports 3 methods in order: API Key (`x-api-key`), Service Token (`x-service-token`), Session (cookies). `@Public()` skips auth. - **Client-side auth**: `authClient` (better-auth client) with `baseURL` pointing to the API, NOT the current app. - **Client-side data**: `apiClient` from `@/lib/api-client` (always sends cookies). - **Server-side data**: `serverApi` from `@/lib/api-server.ts`. - **Server-side session checks**: Proxy to the API's `/api/auth/get-session` endpoint — do NOT instantiate better-auth locally. - **Raw `fetch()` to API**: MUST include `credentials: 'include'`, otherwise 401. ## API Architecture We are migrating away from Next.js server actions toward calling the NestJS API directly. ### Simple CRUD operations Client components call the NestJS API via custom SWR hooks. No server action wrapper needed. ### Multi-step orchestration When an operation requires multiple API calls (e.g., S3 upload + PATCH), create a Next.js API route (`apps/app/src/app/api/...`) that orchestrates them. ### What NOT to do - Do NOT use server actions for new features - Do NOT keep server actions as wrappers around API calls - Do NOT add direct database (`@db`) access in the Next.js app for mutations — always go through the API - Do NOT use `useAction` from `next-safe-action` for new code ### API Client - Server-side (Next.js API routes/pages): `serverApi` from `apps/app/src/lib/api-server.ts` - Client-side (hooks): `apiClient` / `api` from `@/lib/api-client` ### API Response Format - **List endpoints**: `{ data: [...], count, authType, authenticatedUser }` → access via `response.data.data` - **Single resource endpoints**: `{ ...entity, authType, authenticatedUser }` → access via `response.data` - Both `apiClient` and `serverApi` wrap in `{ data, error, status }` ## API Endpoint Contract (MCP-friendly) Every customer-facing endpoint in `apps/api/src/` flows into three systems: the OpenAPI spec (`packages/docs/openapi.json`), the MCP server published as `@trycompai/mcp-server` on npm, and the runtime `ValidationPipe`. The full contract is in [.claude/skills/api-endpoint-contract/SKILL.md](.claude/skills/api-endpoint-contract/SKILL.md) (auto-loaded by Claude) and [.cursor/rules/api-endpoint-contract.mdc](.cursor/rules/api-endpoint-contract.mdc) (auto-loaded by Cursor). The short version every body-accepting endpoint must follow: 1. **DTOs are classes** — never interfaces, never inline `@Body() body: { ... }`. Interfaces are erased at runtime and produce empty MCP schemas. 2. **Two decorator stacks per field** — `@ApiProperty` (or `@ApiPropertyOptional`) for the OpenAPI/MCP schema **and** class-validator (`@IsString`, `@IsOptional`, `@IsObject`, `@IsArray`, etc.) for the ValidationPipe. With only one stack, requests are rejected with *"property X should not exist"* or the MCP tool ships with empty input. 3. **Add `@ApiBody({ type: DtoClass })`** on the endpoint — `@nestjs/swagger` does not reliably infer it from `@Body()` alone. 4. **`@ApiOperation.description` ≤ 240 chars** — `apps/api/src/openapi/seo-text.ts` truncates at a word boundary; longer text loses its actionable instruction. 5. **Override the MCP tool name** when the auto-derived name is ugly: `@ApiExtension('x-speakeasy-mcp', { name: 'kebab-name' })`. 6. **No `SessionOnlyGuard`** on agent-callable endpoints — API-key callers get 403 and the MCP tool fails for customers. 7. **Long-running ops are async** — return a run handle (`runId`, status, counts) and tell the agent the poll target in the description. 8. **File uploads from agents use presigned URLs** — accept an `s3Key` field (read via `UploadsService.readUploadAsBase64`); never accept inline base64 from the MCP tool. 9. **Sensitive paths (e.g. `/credentials`)** are deny-listed from public docs in `apps/api/src/openapi/public-docs-quality.ts` — that's intentional, don't fight it. 10. **SSE / binary responses** can't be consumed by MCP — disable the tool in `apps/mcp-server/.speakeasy/mcp-uploads-overlay.yaml` while keeping the HTTP endpoint for the web UI. 11. **Every endpoint needs a meaningful `@ApiOperation({ summary, description })`** — required and **CI-enforced** (`openapi-docs.spec.ts` fails the build if a public op is missing one). The hosted MCP uses **dynamic toolsets**: the agent finds a tool by semantic-searching names + descriptions, so a missing/weak description makes the tool effectively undiscoverable. Write the description for the agent deciding whether to call the tool — what it does + when to use it. After adding an endpoint: `bun run --filter '@trycompai/api' dev` regenerates `packages/docs/openapi.json` on boot — **commit it with your PR**. The daily Speakeasy CI reads from that file; if it's stale, your endpoint never reaches MCP customers. ## RBAC ### Permissions Model - Flat `resource:action` model (e.g., `pentest:read`, `control:update`) - Single source of truth: `packages/auth/src/permissions.ts` - Built-in roles: `owner`, `admin`, `auditor`, `employee`, `contractor` - Custom roles: stored in `organization_role` table per organization - Multiple roles per user (comma-separated in `member.role`) ### Multi-Product Architecture - **Products** (compliance, pen testing) are org-level subscription/feature flags — NOT RBAC - **RBAC** controls user access within products - `app:read` gates the compliance dashboard; `pentest:read` gates security product - Portal-only resources (`policy`, `compliance`) do NOT grant app access ### API Endpoint Requirements Every customer-facing API endpoint MUST have: ```typescript @UseGuards(HybridAuthGuard, PermissionGuard) // at controller or endpoint level @RequirePermission('resource', 'action') // on every endpoint ``` - Controller format: `@Controller({ path: 'name', version: '1' })`, NOT `@Controller('v1/name')` - `@Public()` for unauthenticated endpoints (webhooks, etc.) - The `AuditLogInterceptor` only logs when `@RequirePermission` metadata is present ### Frontend Permission Gating - **Nav items**: Gate with `canAccessRoute(permissions, 'routeSegment')` - **Rail icons**: Gate product sections (Compliance, Security, Trust, Settings) by permission - **Mutation buttons**: Gate with `hasPermission(permissions, 'resource', 'action')` - **Page-level**: Every product layout uses `requireRoutePermission('segment', orgId)` server-side - **Route permissions**: Defined in `ROUTE_PERMISSIONS` in `apps/app/src/lib/permissions.ts` - No manual role string parsing (`role.includes('admin')`) — always use permission checks ### Permission Resources `organization`, `member`, `control`, `evidence`, `policy`, `risk`, `vendor`, `task`, `framework`, `audit`, `finding`, `questionnaire`, `integration`, `apiKey`, `trust`, `pentest`, `app`, `compliance` ## Design System - **Always prefer `@trycompai/design-system`** over `@trycompai/ui`. Check DS exports first. - `@trycompai/ui` is the legacy library being phased out — only use as last resort. - **Icons**: `@trycompai/design-system/icons` (Carbon icons), NOT `lucide-react` - **DS components that do NOT accept `className`**: `Text`, `Stack`, `HStack`, `Badge`, `Button` — wrap in `
` for custom styling - **Layout**: Use `PageLayout`, `PageHeader`, `Stack`, `HStack`, `Section`, `SettingGroup` - **Patterns**: Sheet (`Sheet > SheetContent > SheetHeader + SheetBody`), Drawer, Collapsible - **After editing any frontend component**: Run the `audit-design-system` skill to catch `@trycompai/ui` or `lucide-react` imports that should be migrated ## Data Fetching - **Server components**: Fetch with `serverApi`, pass as `fallbackData` to client - **Client components**: `useSWR` with `apiClient` or custom hooks (e.g., `usePolicy`, `useTask`) - **SWR hooks**: Use `fallbackData` for SSR initial data, `revalidateOnMount: !initialData` - **`mutate()` safety**: Guard against `undefined` in optimistic update functions - **`Array.isArray()` checks**: When consuming SWR data that could be stale ## Testing - **Every new feature MUST include tests.** No exceptions. - **TDD preferred**: Write failing tests first, then make them pass. - **App tests**: Vitest + @testing-library/react (jsdom environment) - **API tests**: Jest with NestJS testing utilities - **Permission tests**: Test admin (write) and read-only user scenarios - **Run from package dir**: `cd apps/app && bunx vitest run` or `cd apps/api && bunx jest` ## Database - **Schema**: `packages/db/prisma/schema/` (split into files per model) - **IDs**: Always use prefixed CUIDs: `@default(dbgenerated("generate_prefixed_cuid('prefix'::text)"))` - **Migrations**: `cd packages/db && bunx prisma migrate dev --name your_name` - **Multi-tenancy**: Always scope queries by `organizationId` - **Transactions**: Use for operations modifying multiple records ## Git - **Conventional commits**: `(): ` (imperative, lowercase) - **Never use `git stash`** unless explicitly asked - **Never skip hooks** (`--no-verify`) - **Never force push** to main/master ## Forms - All forms use **React Hook Form + Zod** validation - Define Zod schema first, infer type with `z.infer` - Use `Controller` for complex components (Select, Combobox) - Never use `useState` for form field values