Northstar Platform
A production-grade, open-source service-marketplace backend — built end-to-end with enterprise-style architecture, security, testing discipline, and deployment readiness. The code is public.
Keyboard: ← → switch layers · Home End first/last
Switch between the full story and engineering evidence.
Northstar Platform
A production-grade backend system demonstrating how I design, secure, test, and ship real platforms.
Overview
Northstar is a service marketplace backend built to production standards. This is not a tutorial project or proof-of-concept—it is a complete, deployable system with enterprise-grade architecture, security, and testing.
The codebase exists specifically for technical evaluation. Every architectural decision, security implementation, and test case is inspectable.
Problem Statement
Modern service marketplaces require infrastructure beyond basic CRUD operations:
- Multi-role authorization — Different access levels for customers, providers, staff, and administrators
- Complex workflows — Service requests move through multiple states with validation at each transition
- Background processing — Notifications, cleanup tasks, and async operations
- Audit requirements — Complete traceability for compliance
- Horizontal scalability — Architecture that scales without rewrites
Northstar addresses these requirements with a clean, modular architecture.
Architecture
System Design
The interactive map above traces a request end to end: the NestJS API gateway authenticates and validates, a centralized RBAC guard authorizes, domain services run the business logic, and durable work is handed to a BullMQ queue. Switch between Services, Security path, Deployment, Request lifecycle, and Auth flow to see how modules, authorization, runtime topology, and workflow states fit together.
Key Decisions
Layered Architecture — Controllers handle HTTP concerns only. Services contain business logic. Prisma handles data access. No leaky abstractions.
Event-Driven Patterns — Domain events decouple workflows. A service request status change triggers notifications without tight coupling.
Repository Pattern — Prisma provides type-safe queries. Data access is abstracted, testable, and replaceable.
DTO Validation — Every API input is validated through class-validator DTOs. Invalid requests never reach business logic.
Engineering depth
Everything below is inspectable in the Northstar codebase — schema, guards, DTOs, and module boundaries. I include excerpts so reviewers can evaluate judgment without cloning the repo first.
Engineering artifacts
// prisma/schema.prisma — core workflow entities (excerpt)
enum UserRole { ADMIN BUSINESS STAFF CUSTOMER }
enum ServiceRequestStatus {
DRAFT SUBMITTED IN_REVIEW ACCEPTED
IN_PROGRESS COMPLETED CANCELLED
}
model ServiceRequest {
id String @id @default(uuid())
customerId String
title String
description String
status ServiceRequestStatus @default(DRAFT)
priority Int @default(0)
metadata Json?
submittedAt DateTime?
customer CustomerProfile @relation(...)
responses ProviderResponse[]
notes Note[]
@@index([status])
@@index([status, createdAt])
@@map("service_requests")
}
model AuditLog {
userId String
action String
resource String
resourceId String
metadata Json?
// immutable trace for status transitions
}Architecture evolution
v1 — feature modules
Started with NestJS feature folders (auth, users, service-requests) wired directly to Prisma. Worked for CRUD, but authorization logic started leaking into services.
Problem
Service-request endpoints used JwtAuthGuard globally, while admin and provider routes used RolesGuard. Role checks for workflow transitions lived inside ServiceRequestsService.updateStatus — correct behavior, inconsistent enforcement surface.
v2 — guard layer + domain events
Centralized RBAC in RolesGuard + @Roles() on sensitive controllers. Kept workflow-specific rules in the service where they belong (state machine transitions). Async notifications moved to EventEmitter → BullMQ workers with idempotency keys.
Current
Modular monolith: eight bounded contexts (auth, service-requests, provider-responses, admin, jobs, plus shared guards and observability), global JwtAuthGuard + throttling, Prisma for data access, BullMQ for durable async work. The code is the evidence.
Engineering mistakes
Real corrections from building this system — not resume polish.
Mixed authorization surfaces early on
- What broke
- Admin and provider controllers got RolesGuard, but service-requests relied on inline role checks inside the service. Easy to miss a path when adding endpoints.
- What I changed
- Documented the split explicitly: declarative @Roles() for route-level capabilities, service-layer checks for workflow state transitions. Added negative E2E cases for denied roles.
First notification path was synchronous
- What broke
- Emitting email work inline on status change blocked the HTTP response and duplicated send logic when retries happened.
- What I changed
- Moved to domain events (service-request.submitted / completed) consumed by BullMQ email processors with idempotencyKey deduplication.
In-memory idempotency in the email worker
- What broke
- EmailProcessor tracks processed idempotency keys in a process-local Set — fine for a single worker demo, lost on restart or useless across replicas.
- What I changed
- Documented as a known limitation. Production path: Redis-backed dedupe or unique constraint on idempotency key — planned before multi-worker deploy.
Rejected alternatives
Judgment means saying no with a reason, not picking the trendy option.
| Decision | Alternatives | Why rejected | Tradeoff accepted |
|---|---|---|---|
| JWT + refresh tokens | Server-side sessions, OAuth-only | Sessions add Redis session store ops for a stateless API. OAuth-only was out of scope — marketplace roles are first-party accounts, not social login. | Token rotation and blacklist logic are on us; acceptable for this API shape. |
| Modular monolith | Microservices from day one | No independent scaling requirement at build time. Splitting early would have doubled deploy and observability cost with zero traffic. | Horizontal scaling is container-level until a module justifies extraction. |
| BullMQ + Redis | In-process queue, cron-only | Notifications and cleanup must survive process restarts. Cron alone cannot retry failed sends with backoff. | Redis becomes a hard dependency — monitored like Postgres. |
| RBAC at guard layer (where routes are role-scoped) | Per-controller if-checks, attribute-level ACL | Scattered if-checks drift. Full ACL was overkill for four fixed roles. | Workflow transitions still need service-layer rules — guards alone are not enough. |
What I would do at 10× scale
Northstar is sized for a real launch, not hyperscale. Below is how I would evolve it if load outgrew a single deploy — labeled as a future path, not current production metrics.
Current design
- Single NestJS deploy with domain modules (auth, service-requests, provider-responses, jobs, admin)
- PostgreSQL primary with Prisma; Redis for cache + BullMQ
- Stateless API containers behind a load balancer
Bottlenecks at 10×
- Shared Postgres write path on service_requests and audit_logs
- Single Redis instance for queue + cache
- Workflow authorization partially in service layer — harder to audit at scale without tracing
How I'd evolve it
- Read replicas for list/filter endpoints before any service split
- Cache hot list queries (status + createdAt indexes already in schema)
- Extract notifications + jobs into a dedicated worker pool; partition BullMQ queues by job type
- Split highest-traffic module (service-requests) only when metrics justify independent deploy cadence
- Replace in-memory idempotency with Redis SET NX before running multiple email workers
Capacity note: Designed to support thousands of concurrent API users on modest infrastructure; architecture leaves room for service decomposition without rewriting domain logic.
Security Implementation
Security is foundational, not bolted on.
Authentication & Authorization
| Layer | Implementation |
|---|---|
| Identity | JWT with refresh token rotation |
| Authorization | RBAC with 4 distinct roles |
| Password Storage | bcrypt (cost factor 10) |
| Session Management | Stateless with token blacklisting |
Role-Based Access Control
Four roles, each scoped to the smallest set of capabilities it needs:
| Role | Capabilities |
|---|---|
| Admin | Full system configuration, user management, audit-log access, all resources |
| Business (Provider) | Respond to service requests, manage business profile, view own responses |
| Staff | Review incoming requests, manage request workflows, access operational reports |
| Customer | Create service requests, manage own requests, view request history |
API Security
- Rate limiting (100 req/min per client)
- Helmet security headers
- CORS configuration
- Input sanitization
- Audit logging for sensitive operations
Data Model
Core Entities
User — Identity with role assignment and status management
ServiceRequest — Primary business entity with workflow states:
- DRAFT → SUBMITTED → IN_REVIEW → ACCEPTED → IN_PROGRESS → COMPLETED
- CANCELLED (terminal state from any active state)
ProviderResponse — Quotes and proposals from business users
AuditLog — Immutable record of system events with metadata
Database Design
- 12+ strategic indexes for query performance
- Soft deletes for data recovery
- JSON fields for flexible metadata
- Proper foreign key constraints
- Cascading delete rules
Background Processing
Job Queue Architecture
BullMQ handles async operations:
- Email notifications — Queued to prevent request blocking
- Audit log cleanup — Scheduled removal of old records
- Retry logic — Exponential backoff for transient failures
- Idempotency — Duplicate job prevention
Observability
- Structured logging with Pino
- Correlation IDs across requests
- Prometheus-compatible metrics endpoint
- Health check endpoints for orchestration
Testing Strategy
Test Coverage
Unit Tests: 13+ test cases
E2E Tests: 15+ integration tests
Coverage: Comprehensive across modules
What's Tested
- Service layer business logic
- Authorization guard behavior
- API contract validation
- Error handling paths
- Database operations
Technology Stack
| Component | Technology | Purpose |
|---|---|---|
| Framework | NestJS 10.3 | Modular Node.js framework |
| Language | TypeScript 5.3 | Type safety |
| Database | PostgreSQL 16 | Primary data store |
| ORM | Prisma 5.7 | Type-safe queries |
| Cache | Redis 7 | Session, queue backing |
| Queue | BullMQ 5.0 | Background jobs |
| Auth | Passport + JWT | Authentication |
| Validation | class-validator | DTO validation |
| Logging | Pino | Structured logs |
| Docs | Swagger/OpenAPI | API documentation |
Deployment
Infrastructure Requirements
Minimum:
- Node.js 20+
- PostgreSQL 12+
- Redis 6+
Production:
- Docker/Kubernetes deployment
- Load balancer
- SSL termination
- Database connection pooling
Configuration
Environment-based configuration with validation at startup. Missing required variables cause immediate failure with clear error messages.
Project Metrics
- 100+ TypeScript files with strict typing
- 8 domain modules with clear boundaries
- 20+ API endpoints with full documentation
- 28+ test cases across unit and E2E
- 15+ documentation files
Lessons Learned
- Authorization is a cross-cutting concern, not a feature. Centralizing it early made every later endpoint cheaper and safer to add.
- Idempotency is easier designed in than retrofitted. Treating background jobs as "may run more than once" from the start removed a whole class of bugs.
- Tests are design feedback. The parts that were hard to test were the parts that were poorly factored — the test suite kept the boundaries honest.
For a deeper postmortem on the authorization split and async notification path, see When Authorization Logic Split Across Two Layers.
Future Improvements
- Split the highest-traffic modules into independently deployable services once load justifies the operational cost.
- Add distributed tracing across the API and queue workers for end-to-end visibility.
- Introduce contract tests at module boundaries to make a future service split safe.
What This Demonstrates
This project proves capability in:
- System Design — Clean architecture that scales
- Security Engineering — Defense in depth, not afterthought
- Testing Discipline — Comprehensive coverage, not checkbox
- Production Thinking — Deployment-ready, not demo-only
- Documentation — Clear, maintainable, professional
Role: Backend Architect & Developer
Status: Production-Grade Build
License: MIT