Imported from Nucleo-Estudantes-Informatica-ISEP/unclassed (
AGENTS.md). Install upstream withnpx skills add Nucleo-Estudantes-Informatica-ISEP/unclassed. Copyright stays with the author.
AGENTS.md
Reference for AI agents and humans working in this repository. README.md covers setup; this file covers workflow, stack, layer boundaries, documentation, and operational gotchas.
Contribution workflow
For each task:
- Always create a branch from
devnamed<type>/<short-kebab-case-description>, following Conventional Branch. Never use an untyped branch name:feature/orfeat/— new functionalitybugfix/orfix/— bug fixeshotfix/— urgent production fixesrelease/— release preparationdocs/— documentation-only changeschore/— tooling, configuration, and other maintenance
- Commit with Conventional Commits (
feat:,fix:,docs:,refactor:,test:,build:,chore:, ...):- No AI co-author trailer.
- Subject under 72 characters.
- Split unrelated changes into separate commits.
- Push branch and open pull request into
dev, nevermain.
CI/CD and test-first workflow
- Prefer TDD for bug fixes, matching rules, state transitions, locks, and authorization: first add a focused failing regression, implement the smallest correction, then refactor while the suite stays green. If a failure cannot be reproduced without a hosted dependency, document the limitation and add the nearest deterministic test plus a staging procedure.
- Every non-trivial behavior change needs a regression test. A green build without tests is not completion.
- PRs target
dev; reviewed release promotion controls production. Dependabot targetsdev, groups patch/minor maintenance, and leaves major framework/toolchain migrations for explicit planned work. - Required CI uses a frozen install and runs lint, typecheck, all tests, schema validation, production build, a non-root Docker image build, Gitleaks, and CodeQL on every PR. Never disable or bypass a gate to make a PR green.
- Production schema changes require
schema:audit, a backup,schema:deploy, and post-deploy verification. Only the selected scheduler owner may run internal jobs.
Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16, App Router, React 19 |
| Language | TypeScript |
| Styling | Tailwind CSS 4, Radix UI, shadcn-style primitives |
| Database | MongoDB via Prisma 6 (prisma/schema.prisma) |
| Auth | NextAuth 5 beta + AuthNEI OIDC through ZITADEL |
| Validation | Zod; React Hook Form in client forms |
| Matching | Pure graph/algorithm domain core with a Prisma-backed application orchestrator |
| Background work | In-process cron scheduler with MongoDB-backed locks |
| Nodemailer | |
| Package manager | pnpm 9 (packageManager in package.json) |
| Deploy | Docker standalone build, Docker Compose, Vercel config |
Node.js >=20.9.0 required. Prefer pnpm; package-lock.json is legacy and pnpm-lock.yaml is canonical.
Common commands
pnpm dev # Next.js development server
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
pnpm test # Vitest over every **/*.{test,spec}.ts file
pnpm build # production build; see Gotchas
pnpm generate # Prisma client generation
pnpm sync # compatibility alias for schema:deploy
pnpm schema:validate # validate schema without touching MongoDB
pnpm schema:audit # report index/data conflicts without mutation
pnpm schema:deploy # validate, db push, and apply versioned MongoDB indexes
pnpm seed # seed subjects and classes
pnpm start # serve production build
Removed legacy populate, test-cron, and test-cron-system commands must not be reintroduced unless their implementation and tests are committed in the same change.
Architecture
Layout
src/
├── application/ # use-case orchestration and persistence/notification adapters
├── app/ # App Router pages, layouts, and API routes
├── components/ # shared/client UI
├── config/ # app configuration
├── context/ # React context providers
├── domain/ # pure graph structures and matching algorithms (never Prisma)
├── hooks/ # client hooks
├── lib/ # Prisma singleton, OIDC helpers, startup, request-auth (apiAccess), generic utilities
│ └── components/ui/ # reusable UI primitives
├── schemas/ # reusable Zod schemas
├── services/ # matching, cron, mail, session, cache, trigger logic
└── types/ # shared and NextAuth type declarations
prisma/ # MongoDB schema, seed, reset helper
data/ # static subject data
docs/domain-model/ # PlantUML domain diagram
Request and data layers
Routes currently own request orchestration: authenticate, validate input, enforce authorization, call Prisma/services, return NextResponse. Preserve auth and ownership checks on every new or edited mutating route.
- Use
src/lib/prisma.tsfor normal application database access. It is the shared Prisma singleton; never create a newPrismaClientin a route or ordinary service. CronSchedulerowns polling/timers only.src/services/cron/owns registry state, database leases, execution records/stats, and job business handlers; all database-backed modules reuse the shared Prisma singleton.- Prisma and server services belong only in server-side code—never client components or browser hooks.
- Put pure domain logic in
src/domain/, use-case orchestration insrc/application/, and reusable infrastructure logic insrc/services/; do not duplicate logic across API routes. - Put reusable Zod schemas in
src/schemas/. Legacy inline schemas exist, but do not create a second copy of a rule already defined there. - Reuse
src/lib/components/ui/primitives and existing Tailwind patterns before adding UI abstractions.
Matching domain
- A
SingleSwapRequestis scoped to a subject/current class; itsgraphPartitionissubject-<subjectId>. - A
BundleSwapRequestmoves a student across a year's classes; its partition is year-based. MatchingOrchestratorloads requests, delegates compatibility/cycle work tosrc/domain/matching/algorithms.ts, recordsMatchdocuments, and manages provisional-match upgrades/expiry.src/domain/graph/andsrc/domain/matching/stay pure and synchronous; never import Prisma, email, or framework modules there.- Users with blocking accepted matches cannot create new requests. Reuse
hasBlockingAcceptedMatch(). - Creating a request calls
triggerImmediateMatching()asynchronously. Keep matching non-blocking: request creation must not wait for background matching. - Batch matching, provisional cleanup, and health checks run through
CronJobHandlers;CronSchedulerschedules them andJobLockusesCronLockdocuments to prevent concurrent execution. Do not bypass the lock module.
Authentication and authorization
Three distinct mechanisms coexist. Do not conflate them:
- External identity: AuthNEI OIDC via ZITADEL in
src/auth.ts. The provider supplies verified identity claims;syncLocalUserFromOidc()creates or links localUser/UserIdentityrecords. - Application session: NextAuth JWT session.
getServerSession()resolves the NextAuth session, then reloads the local database user. Use it for user/admin authorization;session.idis the local MongoDB user id andsession.roleisUSERorADMIN. - Scheduled-machine access:
CRON_SECRETbearer authentication, checked throughauthorizeRequest()/hasValidCronSecret(). It is for trusted cron callers, not a user session and not authorization for normal browser routes.
Rules:
- Preserve the ZITADEL
email_verifiedgate. - Local
User.passwordexists for compatibility but active sign-in is OIDC. Do not add a parallel password-login flow without an explicit product decision. - Use
authorizeRequest()(src/lib/apiAccess.ts) for every route's session/admin/cron decision and same-origin checks. It is the single authorization layer for the app; do not add a second wrapper or hand-rollgetServerSession()/role checks inline in a route. - For session-authenticated writes, pass
enforceSameOriginForSessionWrites: trueso unsafe methods (non-GET/HEAD/OPTIONS) are rejected with403when the request's origin doesn't match the app's. - Never expose OIDC tokens,
AUTH_SECRET,CRON_SECRET, SMTP credentials, or database URLs to clients or logs.
API conventions
- Validate request bodies with Zod.
- Check ownership server-side—never trust browser-supplied
userId. - Validate referenced MongoDB documents before creating relationships.
- Return explicit response statuses:
400invalid input,401no session,403insufficient authority/origin,404missing resource,409state conflict,500unexpected failure. - Use the established failure shape
{ error: string }for new routes. Legacy routes vary; do not spread that inconsistency. - Keep Portuguese wording consistent in user-facing pages and API messages. Code identifiers and comments remain English.
- Use
@/forsrc/imports. Keep imports formatted by Prettier. - Avoid
any; ESLint treats explicitanyas an error. - Do not make an internal HTTP request from server code when an existing service can be called directly.
Documentation standard
Documentation should answer a future contributor's first question without duplicating implementation details.
- Update
README.mdwhen a user-visible capability, setup prerequisite, environment variable, script, route, deployment requirement, or operational workflow changes. - Update this
AGENTS.mdwhen the stack, layer map, contribution/verification workflow, architectural boundary, or persistent gotcha changes. - Keep documentation adjacent to its audience: public setup and operations in
README.md; contributor rules and architecture here; domain diagrams indocs/domain-model/; code-specific rationale beside the code. - Document why for non-obvious constraints, especially auth boundaries, lifecycle-managed clients, cron locks, data integrity rules, and security decisions. Do not restate code line-by-line.
- Keep examples runnable and command names synchronized with
package.json. - In the same change that alters behavior, update relevant docs. Do not defer known documentation drift to a follow-up.
- Refresh this guide when the matching-engine refactor lands; its layer map and gotchas describe the current implementation, not a planned target architecture.
Database and environment
The datasource is MongoDB, which Prisma Migrate does not support. Schema changes use prisma db push through pnpm schema:deploy (pnpm sync is only a compatibility alias), with ordered records in prisma/schema-changes/, a hash in prisma/schema-manifest.json, and explicit index application. Update the schema, add the next record/hash, regenerate Prisma Client, and keep seed data compatible.
Schema and production-data changes are consequential. Do not run pnpm schema:deploy, pnpm sync, pnpm seed, or prisma/reset.ts against a shared/production database without explicit authorization, a backup, a clean pnpm schema:audit, and a confirmed DATABASE_URL. Preserve MongoDB @db.ObjectId compatibility with existing data.
Read environment variables server-side only. .env.example is source of truth for documented configuration; update it when adding/removing/renaming a variable.
Verification
Before finishing a change:
- Run
pnpm lint. - Run
pnpm typecheck. - Run
pnpm testfor every change. Prefer writing the focused regression first when practical. - Run
pnpm schema:validateandpnpm buildfor configuration, route, schema, or rendering changes. Typecheck remains a separate required gate. - Run
actionlint .github/workflows/*.ymlfor GitHub Actions changes. - Exercise changed behavior through
pnpm dev: interact with UI changes and make a real request for API changes, checking both response body and status. - State exactly what could not be exercised (for example, real OIDC, SMTP, cron, or production MongoDB) rather than implying it passed.
pnpm test runs Vitest over every *.test.ts/*.spec.ts file. Colocate a test next to the file it covers; import test from vitest, and use either Vitest expectations or node:assert. Coverage is still thin — add a focused regression test for non-trivial pure logic or a regression fix, but don't assume prior behavior is covered just because the suite is green.
Gotchas
CronSchedulerstart/stop manages timers only; registry, lock, execution-store, and handler changes belong in their modules undersrc/services/cron/.ENABLE_CRON_SCHEDULER=truestarts in-process jobs. Avoid multiple local instances against one database unless testing lock behavior.- A green
pnpm buildis not a substitute for the separate requiredpnpm typecheckgate. /api/cron/*accepts the cron bearer secret; admin screens/routes require anADMINlocal session. Keep those boundaries separate.src/app/api/test-matches/route.dev.tsandprisma/reset.tsare development/destructive surfaces.next.config.tsonly recognizes the compounddev.tsroute extension in the development server, and the route must also useauthorizeRequest({ devOnly: true }). Preserve both controls.scripts/is ignored by Git. Do not put required product code or tests there unless its ignore rule changes in the same scoped task.- Docker relies on Next standalone output. Verify deployment-sensitive environment/startup changes with
pnpm buildand, when practical, the Docker path.