Architecture
How the Next.js application, shared libraries, Prisma, PostgreSQL, and object storage fit together
ROUNDTABLE is a Next.js App Router application. A browser sends a request to a page or route handler; the handler authenticates and validates it; shared libraries apply matching, notification, search, or storage rules; Prisma talks to PostgreSQL; and S3-compatible storage holds private uploads. The docs site is a separate Next.js application in its own repository and does not import the main app at runtime.
Technology map
| Layer | Repository location | Responsibility |
|---|---|---|
| Browser UI | src/app/portal, src/app/gov | Forms, dashboards, lists, and client interactions. |
| App Router pages | src/app/**/page.tsx | Server-rendered page composition and route-level access checks. |
| API handlers | src/app/api/**/route.ts | JSON/multipart boundary, Zod validation, auth, Prisma operations. |
| Shared policy | src/app/api/gov/_lib/helpers.ts | Government role gates, errors, and audit helpers. |
| Domain services | src/lib/auth.ts, matching.ts, notify.ts, search.ts | Reusable behavior shared across routes. |
| ORM | src/lib/prisma.ts and generated client | Typed PostgreSQL access. |
| Database | PostgreSQL | Durable models and relations in prisma/schema.prisma. |
| Object storage | src/lib/s3.ts | Private upload writes/reads through S3 API. |
| Infrastructure | deploy/ | Terraform AWS path and portable Compose path. |
Source tree walkthrough
| Directory/file | What a new contributor should know |
|---|---|
src/app/api/auth | NextAuth catch-all credentials route. |
src/app/api/portal | Industry registration, profile, submissions, calls, announcements, and file download. |
src/app/api/gov | Government ledger, publishing, organization controls, and administration. |
src/app/api/notifications | List, read, unread count, settings, and reroute. |
src/app/api/export | Enterprise-admin data exports. |
src/app/api/search | Government global search endpoint. |
src/app/gov | Government pages and client components. |
src/app/portal | Industry pages and client components. |
src/lib/auth.ts | Session creation and audience guards. |
src/lib/command-hierarchy.ts | Command scope and hierarchy traversal. |
src/lib/matching.ts | Submission scoring, persistence, and routing. |
src/lib/logger.ts | Structured JSON operational logs and request method/path/status/duration logging. |
src/lib/notify.ts | Notification policy and fan-out. |
src/lib/search.ts | Search backend seam; hybrid full-text then trigram fuzzy implementation. The Prisma substring backend is used when selected via SEARCH_BACKEND, and when pg_trgm is missing the trigram stage degrades to it — the whole search under SEARCH_BACKEND=trigram, only the fallback half under the default hybrid. |
src/lib/s3.ts | S3 client and object writes, encrypted unless UPLOADS_S3_SSE=none. |
src/lib/rate-limit.ts | Single-node registration limiter. |
prisma/schema.prisma | Database models, enums, relations, and defaults. |
prisma/seed.ts | Demo commands, portfolios, users, organizations, calls, and sample records. |
deploy/ | AWS and non-AWS deployment instructions. |
Request and data flow
Route handlers should be thin boundaries. They parse the request, make the authorization decision, call shared behavior, and shape { data } or { error }. This keeps a portal form and an API consumer on the same validation and security path.
Submission lifecycle sequence
Cross-workstream contracts
WS1 (industry intake) owns creation of submissions and calls routeSubmission. WS2 (government ledger) owns engagement, publication, organization status, and administration. WS3 owns matching and notification helpers. WS4 owns search and export. The contracts are function-level rather than separate services, so changes must preserve their callers' response and authorization behavior.
Error and audit conventions
Validation failures are normally HTTP 400 with a generic Invalid request. Missing sessions are 401; role or scope failures are 403; missing records are 404; conflicts are 409; unexpected failures are generic 500 responses with server-side logging. Mutating and security-sensitive paths write AuditLog rows, including authentication, authorization failure, publication, registration, engagement, and routing.
Server and client boundaries
Next.js server components can read protected data during page rendering, but a browser form still submits to an API route. Client components should not import Prisma, bcrypt, or AWS credentials. The route handler is the trust boundary because request bodies, URL IDs, cookies, and multipart filenames are all untrusted input.
The docs site follows the same App Router concept but has no database connection. Its MDX loader creates static documentation pages, and its Mermaid component renders diagrams in the browser with strict Mermaid security settings. This separation prevents a documentation build from accidentally depending on a development database.
Data ownership sequence
For a new feature, identify the owner of every write:
| Write | Owner |
|---|---|
| Organization and first industry user | Portal registration transaction. |
| Submission and optional upload | Portal submission route. |
| Match rows and routing status | routeSubmission. |
| Notification row | notify* helpers. |
| Engagement and note | Government ledger route. |
| Public announcement or call | Government publisher route. |
| Status, hierarchy, and user role | Government admin routes. |
| Audit event | Route/helper that performed the action. |
This table helps avoid duplicating business rules in page components or documenting an operation that the code does not actually perform.
A request-reading exercise
When debugging a feature, follow one request in this order:
- Identify the browser page and the exact URL it calls.
- Open the matching
src/app/api/**/route.tsfile. - Read its session helper and role gate before reading the success branch.
- Read the Zod schema and compare it with the browser payload.
- Follow imported library functions such as
routeSubmission,notify*, or hierarchy helpers. - Identify Prisma writes, transaction boundaries, and audit events.
- Check whether S3 is involved and whether failure is fatal or logged-and-continued.
- Compare the documented response with the actual
NextResponse.jsonbody.
This method keeps documentation grounded in executable behavior rather than in page labels or assumptions about what a conventional Next.js API “usually” does.