Skip to main content
joão

2025 — Case Study

SADEP

Probationary performance evaluation system for Pará state civil servants

TypeScriptNestJS 11Next.js 15React 19Prisma 6BullMQTurborepoPostgreSQL
01

Context

Pará law mandates multi-stage performance evaluations for public servants during their probationary period. SADEP replaced a manual, paper-driven process with a tracked digital workflow — routing evaluations through CESAD commissions, managers, and HR across state agencies.

02

Architecture

Turborepo monorepo. Four apps sharing typed contracts via a common package.

apps/api

NestJS 11 — domain-driven modules, JWT auth, BullMQ producers

apps/web

Next.js 15 App Router, React 19, server components + client islands

apps/worker

BullMQ consumer — async job processing (notifications, PDF generation)

apps/cron

Scheduled tasks — evaluation deadline checks, automated reminders

packages/contracts

Shared TypeScript types for all API request/response shapes

packages/config

Centralised ESLint, TypeScript, and Tailwind configurations

03

Engineering Decisions

01

Refresh token rotation with reuse detection

Each refresh cycle issues a new refresh token and immediately marks the previous one as consumed, recording its successor in replacedBySessionId. If a consumed token is ever re-presented — the signature of a stolen token replayed after the legitimate client already rotated — the entire token family is revoked. This detects theft without rate limiting and without requiring device fingerprinting. Session granularity means targeted revocation is possible without forcing a full logout across all devices.

02

Timing-safe comparison against token oracle attacks

All token equality checks go through Node.js crypto.timingSafeEqual. Standard string equality short-circuits on the first differing byte, leaking how many leading characters of a forged attempt matched. Constant-time comparison eliminates that side-channel. This is non-obvious in a JavaScript runtime where developers rarely think about timing attacks at the application layer.

03

End-to-end type safety via shared contracts

The packages/contracts package exports TypeScript types consumed by both NestJS DTOs and Next.js fetch utilities. Any API shape change — added field, renamed property, changed enum — propagates as a compile-time error in the monorepo before it can reach staging. No code-generation step, no OpenAPI drift, no runtime surprises on field renames.

04

Architecture Decision Records (ADRs)

Non-obvious architectural choices are documented as numbered ADRs with context, decision, and consequences. Why HS256 over RS256 for a single-issuer setup. Why BullMQ for async over direct cron invocation. Why Prisma migrations over schema-first SQL. Future maintainers can read the trade-offs instead of re-litigating settled questions.

05

Co-located domain tests

Tests live next to the domain logic they cover, not in a parallel __tests__ tree. CESAD commission rules, probationary period state transitions, evaluation scoring — each module is tested at its own boundary. A test file that drifts away from its module is immediately visible in code review, keeping coverage honest.