Architecture
nthtime is an Nx 22.5.1 monorepo managed with pnpm 10 workspaces. This page describes the workspace layout, linking strategy, module resolution bridging, and core data flows.
Workspace layout
nthtime/
apps/
web/ Next.js 16 frontend (shadcn/ui, Monaco, Tailwind v3)
libs/
shared/ Pure types: Pack, Challenge, Assertion, Verification, Attempt, Settings
data-access/ Repository interfaces: PackRepository, AttemptRepository, SettingsRepository
verification/ Tree-sitter WASM verification engine (12 evaluators + pipeline)
editor/ Zustand vanilla store (EditorStore), language mapping, time formatting, drafts
services/api/ Spring Boot 3.5 backend (Java 25, PostgreSQL 16)
packs/ Challenge pack JSON files (pack.json + challenges/*.json per pack)
tools/ CLI scripts: validate-packs.ts, seed.ts
docs/ VitePress documentation siteKey boundaries
Boundary Rule
apps/web is the only deployable frontend. Libraries export pure logic and types -- no React imports except where noted. Spring Boot has its own Gradle build and is not managed by Nx.
apps/webis the only deployable application. It consumes all four libraries.libs/packages are framework-agnostic (no React imports except where noted). They export pure logic and types.services/api/is the Spring Boot backend (Java 25, Gradle Kotlin DSL). It has its own build system and is not managed by Nx.packs/contains challenge content as JSON. Validated bytools/validate-packs.tsand seeded to Spring Boot bytools/seed.ts.tools/scripts use direct relative imports (not workspace packages) andfileURLToPath(import.meta.url)for__dirnamebecauseimport.meta.dirnameis undefined undernpx tsx.
Nx Crystal plugins
Nx Crystal plugins auto-infer build, dev, lint, test, and typecheck targets from existing config files (e.g., vite.config.ts, tsconfig.json, .eslintrc). Most project.json files do not need explicit target definitions.
The exceptions are apps/web/project.json, which overrides build and dev targets to pass the --webpack flag to Next.js.
Library linking
Libraries use pnpm workspace protocol and TypeScript project references -- not tsconfig path aliases.
Each library's package.json declares:
{
"name": "@nthtime/shared",
"exports": {
".": "./src/index.ts"
}
}Consumers depend on workspace packages:
{
"dependencies": {
"@nthtime/shared": "workspace:*"
}
}Import with the bare specifier:
import { Pack, Challenge } from '@nthtime/shared';TypeScript resolution works through composite: true and project references in each library's tsconfig.json.
Module resolution bridging
There is a resolution mismatch between libraries and the Next.js app:
| Layer | moduleResolution | Import style |
|---|---|---|
| Libraries | nodenext | .js extensions required |
| apps/web | bundler | No extensions needed |
next.config.js bridges this gap with:
extensionAlias: {
'.js': ['.ts', '.tsx', '.js'],
}This tells webpack to try .ts and .tsx when it encounters a .js import from a library.
next.config.js setup
The Next.js configuration handles several integration concerns:
--webpackflag: Required for bothbuildanddev. Turbopack fails with Nx's dynamicrequire()calls.asyncWebAssemblyexperiment: Enables Tree-sitter WASM grammars to load in the browser. Grammar files (JS, TS, TSX, Python, HTML, CSS) are served fromapps/web/public/tree-sitter/.NormalModuleReplacementPlugin: Rewritesnode:scheme imports (e.g.,node:fs,node:path) used by@nthtime/verification. Combined withresolve.fallback: falseforfs/pathin the browser bundle.- Monaco editor alias:
monaco-editor$is aliased tosrc/lib/monaco-editor-shim.js, which re-exportswindow.monaco. This is necessary becausemonaco-emacscallsrequire('monaco-editor')expecting the AMD bundle. The alias must apply to both server and client builds. output: 'standalone': Produces a minimal production image for Docker.
Data flow
The core user journey follows this path:
Catalog (/) --> Pack (/pack/[slug]) --> Challenge (/challenge/[id]?pack=slug)
|
v
Monaco Editor
|
v
Verification Engine
(Tree-sitter WASM)
|
v
Results View
(pass/fail, diff, hints)- The pack slug is threaded via the
?pack=query parameter throughout navigation (catalog to pack to challenge to results). - The editor store (
@nthtime/editor) manages file state, timer, draft persistence, and view mode (editing|results). - Verification runs client-side using Tree-sitter WASM grammars. The engine has 12 evaluators and a pipeline that processes assertions against parsed ASTs.
- Feedback level (0--4) controls how much detail the results view reveals: L0 = banner only, L1 = pass/fail per assertion, L2 = hints, L3 = details + location, L4 = diff view.
Data access layer
The frontend uses TanStack React Query to call REST endpoints at /api/v1/. Next.js API routes at apps/web/src/app/api/v1/ are thin proxies that forward requests to Spring Boot via apps/web/src/lib/spring-boot-proxy.ts, including the JSESSIONID session cookie for authentication context.
Browser --> Next.js API route (/api/v1/*) --> Spring Boot (http://api:8080/api/*)Client hooks in apps/web/src/hooks/ fetch from /api/v1/ routes via apps/web/src/lib/api-client.ts. The useAuthSession() hook checks auth status by querying /api/v1/auth/session.