A full-stack, multi-tenant candidate compliance platform built as a focused vertical slice of a regulated workforce SaaS platform.
The implementation prioritises tenant isolation, explicit authorisation, auditability, immutable compliance history, reliable asynchronous verification, and governed AI-assisted CV extraction.
- Multi-tenant PostgreSQL data model
- Candidate CRUD with pagination and filtering
- Compliance document CRUD and version history
- PostgreSQL Row-Level Security (RLS) with
FORCE ROW LEVEL SECURITY - Application-layer tenant checks
- JWT authentication
- Explicit permission checks per operation
- Problem Details error responses
- Versioned REST API under
/api/v1 - Swagger/OpenAPI documentation
- Append-only audit ledger
- Database trigger preventing audit UPDATE/DELETE
- Right-to-Work verification using a transactional outbox and in-process poller
- Retry and terminal-failure handling
- Idempotency support for compliance-document creation
- Governed CV extraction with a provider abstraction
- Schema validation of AI output
- Human confirmation/rejection before candidate data is changed
- AI audit records
- Documents-expiring-within-30-days endpoint
- Next.js/React frontend for authentication, candidates, dashboard and AI CV extraction
- Unit, integration and tenant-isolation tests
HTTP request
↓
JWT authentication
↓
TenantContextGuard
↓
PermissionsGuard
↓
Controller
↓
Domain service
↓
TenantTransactionService
↓
SET LOCAL app.tenant_id
↓
PostgreSQL RLS + application tenant filters
↓
Database
Tenant identity is never accepted from the client. It comes from the authenticated user's server-side context.
Tenant-scoped transactions set app.tenant_id before domain queries execute. PostgreSQL RLS then enforces the tenant boundary.
RLS protects:
CandidateComplianceDocumentDocumentVersionAuditEventOutboxEventAIExtractionIdempotencyKey
Audit events contain tenant, actor, action, record type/ID, timestamp and before/after hashes.
The audit ledger is append-only. The database migration revokes UPDATE/DELETE from PUBLIC and installs a trigger that rejects audit-event mutation.
A logical compliance document has associated DocumentVersion records.
Corrections create a new version rather than destructively overwriting history. Each version can reference the version it supersedes.
Verification requests and outbox events are persisted transactionally.
The in-process worker:
- Reads pending events.
- Claims an event.
- Loads tenant-scoped verification state.
- Calls the verifier outside the DB transaction.
- Re-checks state.
- Updates verification/document/version state atomically.
- Writes audit events.
- Marks the event processed.
- Retries failures up to a bounded attempt count.
The verifier is interface-driven so a real provider can replace the mock.
The CV workflow is:
Upload CV
↓
Extract text
↓
Provider
↓
Structured output
↓
Schema validation
↓
PROPOSED
↓
Human review/edit
↓
ACCEPTED / REJECTED
AI output never automatically rejects a candidate.
The current provider is deterministic and local and is bound through the CV_EXTRACTION_PROVIDER NestJS dependency-injection token. It is currently mapped to MockCvExtractionProvider; it is not an environment-variable switch.
The Next.js frontend provides:
- Login
- Dashboard
- Candidate directory
- Candidate creation
- Candidate search/pagination
- Governed CV extraction
- Human review and confirmation of proposed AI data
Frontend role checks are only UX behaviour. Backend authentication, authorisation and tenant isolation remain authoritative.
cd apps/api
npm installCreate apps/api/.env:
DATABASE_URL=postgresql://<user>:<password>@localhost:5432/<database>
JWT_SECRET=<local-development-secret>
PORT=8080Then:
npm run generate
npx prisma migrate deploy
npm run db.seed
npm run start:devSwagger:
http://localhost:8080/api/docs
cd apps/web
npm installCreate apps/web/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:8080/apiThen:
npm run devTwo tenants are seeded for isolation testing.
Acme Recruitment
admin@acme.test
recruiter@acme.test
Global Talent Partners
admin@global.test
recruiter@global.test
Password for all seeded users:
Password123!
These credentials are for local development/testing only.
Backend unit/integration tests:
cd apps/api
npm test -- --runInBandE2E tests:
npm run test:e2e -- --runInBandCoverage:
npm run test:covBuild:
npm run buildFrontend:
cd apps/web
npm run lint
npm run buildThe test suite includes tenant-isolation, permissions, audit, candidate/document, verification and AI extraction coverage.
Endpoints are versioned:
/api/v1/...
Authentication:
Authorization: Bearer <access-token>Global validation uses NestJS ValidationPipe.
Errors are returned using Problem Details, for example:
{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "Validation or domain error",
"instance": "/api/v1/...",
"timestamp": "..."
}This is a time-boxed take-home rather than a production deployment.
The outbox uses an in-process poller rather than a separately deployed worker because the brief permits an in-process equivalent and the approach demonstrates the required transactional outbox and retry semantics without unnecessary infrastructure.
The AI and Right-to-Work providers are mock/local implementations behind interfaces because the brief evaluates integration design and governance rather than access to paid external services.
The frontend focuses on the highest-value demonstration flows rather than attempting to implement a complete recruitment product.
Idempotency is implemented for compliance-document creation. A production implementation would apply the same mechanism consistently across every externally retried write endpoint.
If this moved beyond the take-home, I would add:
- Dedicated queue/worker infrastructure
- Distributed worker coordination
- Exponential retry backoff and dead-letter handling
- Object storage for uploaded CVs
- Malware/content scanning
- Real LLM integration with model governance
- Stronger file-size/content validation
- Structured observability and distributed tracing
- Refresh-token/session management
- Rate limiting
- Cloud secret management
- CI/CD and security scanning
- Database backup/restore procedures
- Broader API-wide idempotency coverage