Architecture
NestFleet is a monorepo with a clean separation between the HTTP API, the background worker system, and the Next.js console. All state lives in a single PostgreSQL database. There are no external message queues — job scheduling is handled by pg-boss, which runs inside PostgreSQL.
High-level overview
| Component | Technology | Responsibility |
|---|---|---|
| API | Hono (TypeScript) | HTTP REST API. Handles all inbound requests: auth, case ingestion, channel webhooks, CRUD. |
| Console | Next.js 15 (App Router) | Operator UI. Server components + client islands. Communicates with the API over HTTP. |
| Worker | Node.js process | Background job executor. Connects to pg-boss and processes AI pipeline jobs. |
| Database | PostgreSQL 16 + pgvector | Primary datastore and job queue (via pg-boss). Vector search for KB matching. |
| Reverse proxy | Caddy | TLS termination, HTTP→HTTPS redirect, routing between API and console. |
Layered architecture
The API and worker both follow a strict three-layer architecture to separate concerns and make individual pieces independently testable:
| Layer | What it does | Example files |
|---|---|---|
| Route (controller) | Parses and validates the HTTP request using Zod, calls the service, returns the HTTP response. No business logic. | src/api/routes/cases.ts |
| Service | Contains all business logic. Orchestrates calls to one or more repositories, dispatches pg-boss jobs, enforces rules. | src/api/services/case.service.ts |
| Repository | Executes SQL queries against the database. Returns typed domain objects. Never called directly from routes. | src/infra/repositories/case.repo.ts |
This structure is enforced by convention — ESLint rules prevent direct database access from route files. Services are unit-tested with mocked repositories. Integration tests exercise the full stack (route → service → real DB).
Worker system and pg-boss
NestFleet uses pg-boss to manage background jobs. pg-boss stores job queues as PostgreSQL tables, eliminating the need for a separate Redis or RabbitMQ service. Jobs survive process restarts, have built-in retry logic, and support at-least-once delivery semantics.
Workers are registered at startup in src/workers/index.ts. Each worker is a function that receives a job payload and returns a result. Workers are registered with a queue name that corresponds to the job type:
boss.work("triage", async (job) => {
await triageWorker(job.data)
})
boss.work("auto_reply", async (job) => {
await autoReplyWorker(job.data)
})Jobs are dispatched from services using the pg-boss client:
await boss.send("triage", { caseId: newCase.id })AI pipeline
The AI pipeline runs as a chain of pg-boss jobs. Each step is independent and can be retried on failure without re-running earlier steps:
| Job | LLM tier | What it does |
|---|---|---|
| triage | Fast (LLM_MODEL_FAST) | Classifies the case: type, severity, confidence. Produces the reasoning trace. Dispatches known_issue_match if confidence is high enough. |
| known_issue_match | Fast (embedding) | Runs a vector similarity search against the knowledge base. Attaches matching articles to the case. Dispatches auto_reply if a close match is found. |
| auto_reply | Standard (LLM_MODEL) | Generates a reply draft using the matched articles as RAG context. Routes to approval queue or sends immediately based on product settings. |
| change_prep | Complex (LLM_MODEL_COMPLEX) | Analyses the case and produces a structured PR draft with affected surfaces and risk assessment. Used for novel bugs. |
| embed_article | Embedding model | Embeds a new or updated KB article and upserts its vector in pgvector. Triggered when an article is created or updated. |
Auth: JWT + RBAC
Authentication is JWT-based. The login endpoint issues a signed access token (short-lived) and a refresh token (long-lived, stored in an httpOnly cookie). All other API routes require a valid Bearer token in the Authorization header.
Authorization is enforced by a requireAuth middleware in src/auth/. It:
- Verifies the JWT signature using
JWT_SECRET - Loads the user and their roles from the database
- Checks the required role(s) for the route
- Attaches the authenticated user to the Hono context for downstream handlers
Every API route calls requireAuth() — there is an ESLint rule (no-unprotected-route) that flags routes missing auth middleware during CI.
Encryption
Sensitive values — LLM API keys, webhook secrets, SMTP passwords — are encrypted at rest using AES-256-GCM before being stored in the database. Encryption is performed bysrc/infra/crypto.ts using Node.js's built-incrypto module with a random IV per value. The ENCRYPTION_KEY env var (64 hex chars = 32 bytes) is the key material. Without it, secrets are stored in plaintext with a startup warning.
Key directories
src/
├── api/
│ ├── routes/ # Hono route handlers (thin controllers)
│ ├── services/ # Business logic
│ └── middleware/ # Auth, logging, error handling
├── workers/
│ ├── index.ts # Worker registration and pg-boss setup
│ ├── triage/ # Triage agent logic
│ ├── auto-reply/ # Auto-reply generation
│ ├── change-prep/ # Change request preparation
│ └── embed/ # Embedding jobs
├── infra/
│ ├── db/
│ │ ├── migrations/ # SQL migration files (up only, sequential)
│ │ └── client.ts # postgres.js connection pool
│ ├── repositories/ # SQL query functions
│ └── crypto.ts # AES-256-GCM encryption helpers
├── auth/
│ ├── jwt.ts # Token sign / verify
│ └── middleware.ts # requireAuth(), requireRole()
└── shared/
├── config.ts # Zod-validated env var schema
└── logger.ts # Structured JSON logger (pino)Database migrations
Migrations are plain SQL files in src/infra/db/migrations/, named sequentially: 001_initial_schema.sql,002_add_cases.sql, etc. The API applies all pending migrations at startup using a simple migration runner — no ORM, no migration framework dependency. Migrations are idempotent (applied only once, tracked in a_migrations table).