Concise, enforceable standards for this app. Enough context to work effectively without bloating agent memory.
- Mission: Manage and analyze red‑team operations and defensive effectiveness.
- Core objects: Operations → Techniques → Outcomes, plus Taxonomy (tags, tools, log sources, crown jewels, threat actors) and MITRE data.
- Users and roles: ADMIN, OPERATOR, VIEWER. Groups are optional to restrict access per operation.
- Tech stack: Next.js 15 (App Router) + TypeScript, tRPC v11 + Zod, Prisma, NextAuth v5, Tailwind.
- Local development runs the Next.js dev server against a local PostgreSQL container; production environments use Docker with Postgres (user provides their own reverse proxy)
npm run db:up— Start local Postgres (deploy/docker/docker-compose.dev.yml)npm run init— Apply migrations + seed baseline datanpm run dev --turbo— Start dev servernpm run db:migrate -- --name <change>— Create a new migration during developmentnpm run db:deploy— Apply committed migrations to the current databasenpm run seed:demo— Populate optional demo taxonomy/operation datanpm run check— Lint + type-check (must be clean)npm run test— Run testsnpm run build— Production build
- Security first: all app routes and tRPC procedures require authentication. No public endpoints other than login.
- Single source of truth: no duplicated APIs for the same data/metric.
- Keep API shapes stable during structural refactors.
- Predictable typing: no
any. Use Zod to validate inputs and Prisma types for DB results. - Prefer small, composable modules; target 300–700 LoC per file.
src/
app/ # Next.js app router
features/ # Domain UI + hooks (operations, analytics, settings, shared)
components/ui/ # Shared UI primitives only
lib/ # Framework‑agnostic utilities and MITRE helpers
server/
api/routers/ # All tRPC routers (entity + analytics)
services/ # Shared DB/service logic
auth/ # NextAuth config/callbacks
db/ # DB bootstrap helpers
test/ # Vitest tests, factories, utilities
types/ # Global ambient types
- Do not create routers under
src/features/**; keep them insrc/server/api/routers/**. - Keep analytics-only logic under
src/server/api/routers/analytics/**(no CRUD). src/lib/**remains React-free.
"paths": {
"@/*": ["src/*"],
"@features/*": ["src/features/*"],
"@server/*": ["src/server/*"],
"@lib/*": ["src/lib/*"],
"@components/*": ["src/components/*"]
}
Use eslint-plugin-boundaries and no-restricted-imports to discourage cross‑feature leaks.
- Use
protectedProcedure/viewerProcedure/operatorProcedure/adminProcedure(no public procedure). - Use the shared access helpers from
src/server/api/access.tseverywhere you need operation scoping:getAccessibleOperationFilter(ctx)— list queriescheckOperationAccess(ctx, operationId, action)— view/modify checks
- Group-based rule: operations are either
EVERYONEorGROUPS_ONLY. Non-admins must belong to at least one of the operation'saccessGroupswhen visibility isGROUPS_ONLY;EVERYONEoperations are visible to all authenticated roles. - Redirect policy: Middleware gates all routes; unauthenticated API calls get 401 JSON, and pages redirect to
/auth/signin(withcallbackUrl). - Layout gating (route group pattern):
- Put all protected pages under
src/app/(protected-routes)/**and add a serverlayout.tsxin that group that callsauth()and redirects to/auth/signinwhen missing. This prevents static prerender. - Do not duplicate auth checks in child layouts/pages under the group. Rely on the group layout for auth.
- Keep
src/app/(protected-routes)/settings/layout.tsxfor the admin-only rule; it should only enforcesession.user.role === ADMIN(assumes auth already passed). - Keep public auth at
src/app/(public-routes)/auth/signin/**. - Demo mode login is optional; when enabled it should expose a single button for the initial admin on the sign-in page.
- The homepage
/is under the protected group and does not need page-levelauth().
- Put all protected pages under
- Routers by concern:
- Entity routers: CRUD and simple queries.
- Analytics router: aggregations and metrics only (no CRUD). Keep analytics here; do not duplicate analytics in entity routers.
- Consistent shapes: list and get endpoints should return the same entity shape.
- Input validation: all endpoints validate with Zod; avoid optional/loose shapes when a stricter one is known.
- Error semantics: prefer
TRPCError({ code, message }); do not leak internal details. - Services layer: Routers validate/auth, then call small service functions for shared DB logic. Services throw
TRPCErroron validation failures and return Prisma-typed results. This avoids duplication across routers and import flows.
- Purpose: keep routers thin, reduce duplication, standardize validation and errors.
- Location:
src/server/services/*(e.g.,operationService.ts,techniqueService.ts). - Use when: logic is reused by multiple routers/flows or validation is nontrivial.
- Avoid when: a trivial, single Prisma call unlikely to be reused.
- Conventions:
- Signature: function(db: PrismaClient, dto) => Prisma-typed entity/result.
- Inputs: routers own Zod validation; services accept already-narrowed DTOs.
- Errors: throw
TRPCError({ code, message }); do not return error objects. - Auth: none in services; use access helpers in routers.
- Shapes: return include/select shapes the UI/tests already rely on.
- Testing: prefer router-level tests; mock
ctx.dband assert service usage, returned shapes, and TRPC errors. Only unit-test services directly when they have substantial logic. - Current services:
operationService.ts:createOperationWithValidations,updateOperationWithValidationstechniqueService.ts:getNextTechniqueSortOrder,createTechniqueWithValidationsthreatActorService.ts:createThreatActor,updateThreatActor,deleteThreatActoroutcomeService.ts:createOutcome,updateOutcome,deleteOutcome,bulkCreateOutcomes,defaultOutcomeIncludegroupService.ts:createGroup,updateGroup,deleteGroup,addMembersToGroup,removeMembersFromGroupuserService.ts:createUser,updateUser,defaultUserSelecttaxonomyService.ts:create/update/deleteforcrownJewel,tag,toolCategory,tool,logSource
- Strict mode; zero
any(useunknown+ narrowing when needed). - Prefer inference over assertions; avoid
as any. - Export canonical context types from
trpc.tsand use them in helpers:TRPCContext— raw contextAuthedContext— non-nullsession(for protected flows)
- DB reads: request the minimum fields needed (
selectover broadincludewhen possible). - DB writes (bulk/restore): validate and whitelist with Zod before persisting.
- Seed/restore: keep side-effecting routines isolated in the server layer (no UI logic).
- Viewer UX is read-only:
- Hide editing affordances, disable DnD, prevent opening editors.
- Present a neutral view (avoid green accent unless signaling success/positive state).
- Destructive actions: place at the bottom with a themed confirmation (no browser popups).
- Keep pages focused: avoid unnecessary “view all”/CTA clutter in headers.
- Source of truth:
docs/dev/STYLE.md. Follow it for tokens, card variants, hover states, and confirmation patterns. - Cards: use neutral default cards (1px border) for all list/content sections; use
variant="elevated"only for overlays/modals. - Hover: navigational cards may show a subtle ring; edit-in-place rows/cards do not have a card-level hover ring and expose InlineActions.
- Destructive actions: always use
ConfirmModal(secondary Cancel, danger Delete). No browserconfirm(). - Heatmaps/metrics: use tokens; avoid hardcoded brand colors.
- Keep STYLE.md current: when you migrate or alter a pattern, tick the checklist items and add a short rule if you had to make a new decision.
- Before merging UI work: scan the affected pages to ensure surfaces, buttons, and confirmations match STYLE.md, then run
npm run check.
- No raw
console.log/console.errorin production paths. Use a tiny logger utility gated by env. - Avoid PII in logs. Include a
requestIdin context where helpful for tracing.
- Push aggregations to the DB when reasonable. Minimize N+1 and heavy
includes. - Cache is optional and local for now (short TTL in-memory) — only where it demonstrably reduces cost.
- Add indexes when access filters become hot paths (e.g., group/tag lookups).
- Run
npm run checkandnpm run teston every PR. - Tests live under
src/test/**with factories insrc/test/factories/**and helpers insrc/test/utils/**. - Split large scenario-dense tests into focused files.
- Access control: helper tests + router-level tests to assert filters are enforced.
- Analytics completeness: return all tactics with zeroed metrics when not executed.
- Validation: inputs fail fast with clear messages.
- Coverage: run
npm run test:coverage; prioritize server routers, services, and shared libs. UI and scripts are excluded. Keep test files small and modular.
- Before coding: run
npm run checkand scan existing patterns. - After coding: ensure
npm run checkandnpm run testare clean. - New endpoints: confirm they fit the router’s concern and reuse shared helpers.
- Treat
prisma/schema.prismaas the source of truth for all schema edits; never hand-edit existing SQL migrations. - Create migrations with
npm run db:migrate -- --name <change>immediately after updating the schema; commit both the schema update and the newprisma/migrations/folder. - Apply committed migrations locally with
npm run db:deploy(ornpm run initon first run) and push the same migrations with your PR—no drift fixes after merge. - Use
npx prisma migrate diff --from-migrations --to-schema-datamodel prisma/schema.prisma --exit-codeif you suspect divergence; CI runs the same guard. - Tests target
TEST_DATABASE_URL(defaultpostgresql://rtap:rtap@localhost:5432/rtap_test) andglobal-setupcreates the DB on demand before runningprisma migrate reset, keeping your main dev database untouched.
- Use
git mvbefore edits to preserve history. - Only create routers under
src/server/api/routers/**. - Do not bypass the services layer with raw Prisma in routers (except trivial one-liners unlikely to be reused).
- Keep updates brief and domain-scoped.
- Shape freeze: do not change endpoint shapes during structural PRs.
- Lib purity: keep
src/lib/**framework-agnostic (no React/UI imports).
These are living guidelines — keep them concise. If something routinely surprises us, add a short rule here rather than duplicating logic in code.
- Operations own visibility:
EVERYONE(all roles) orGROUPS_ONLY(members of at least one linked group). Non-admins inherit access fromaccessGroups; admins bypass the check. - Viewers are read‑only across the UI; Operators can modify operations; Admins can do everything including application settings.
- Groups are optional and can further restrict access to specific operations.
- Analytics respect access: results include only data from accessible operations, but still return the full MITRE tactic/technique set (zero‑filled).
- Pages (App Router):
src/app/** - Domain UI & hooks:
src/features/**(e.g.,operations/components/...) - Shared UI primitives:
src/components/ui/** - API (tRPC):
src/server/api/**routers/*— entity + analytics routersaccess.ts— centralized access helperstrpc.ts— initialization +TRPCContext/AuthedContext
- Services:
src/server/services/** - Auth config:
src/server/auth/** - Database:
prisma/schema.prisma - MITRE data:
data/mitre/enterprise-attack.jsonparsed viasrc/lib/mitreStix.ts; tactic ordering insrc/lib/mitreOrder.ts - Env schema:
src/env.ts - Tests:
src/test/**(Vitest)
- Local run:
npm install && npm run dev --turbo; first DB: see README “Getting Started”. - Endpoints: follow “API Architecture” + “Auth & Access” above; validate with Zod; reuse access helpers; keep shapes consistent.
- UI: server auth, neutral viewer UX, destructive actions with themed confirmation.
- Project overview + setup: README.md
- Design overview: docs/dev/DESIGN.md
- UI style guide: docs/dev/STYLE.md
- This guide: AGENTS.md (source of truth for coding standards)