Architecture
System architecture, tech stack, monorepo structure, and core design decisions for the Remote Eaze DFA solution.
Tech Stack
- Runtime: Node.js v22.15.0+ (ES Modules)
- Package Manager: pnpm v10.30.2+ (Strict Mode)
- Orchestrator: Turbo v2.8.10+
- Backend: Fastify + Zod v4 + Swagger
- Auth: Better Auth (shared across API, Web, and Tools)
- Frontend: React 19 + Vite 7 + TanStack (Router, Query, Form, Table, Virtual) + Zustand + Dexie (PWA)
- Documentation: Fumadocs + Next.js 16
- Database: PostgreSQL + Prisma 7.4 (
@prisma/adapter-pg+pgdriver) + tsup - Cache/Queue: Redis (ioredis + BullMQ)
- Tooling: Biome 2 (Lint + Format), Vitest 4 (Testing), tsup (Bundling), Husky + lint-staged (Git hooks)
- Infrastructure: Docker (Compose), GitHub Actions CI
Repository Structure
The project follows a strict monorepo pattern using pnpm workspaces.
.
├── apps
│ ├── api # Fastify backend (consumes packages)
│ ├── web # React + Vite + TanStack (PWA client)
│ └── docs # Fumadocs + Next.js (documentation site)
├── packages
│ ├── config # Environment variable loading & validation (dotenv + Zod)
│ ├── database # Prisma 7 schema, client singleton, migrations
│ ├── custom-fields # Shared "Brain" (rules, formulas, validation) — DB-free
│ ├── shared # Common types, constants, and date utilities (Luxon)
│ └── ui # Design system (Radix UI, HugeIcons, CVA, Tailwind)
├── tools
│ └── scripts # Seed & maintenance scripts (system-admin, ISO data, cleanup)
├── docker
│ ├── Dockerfile.api # Multi-stage API build
│ ├── Dockerfile.web # Vite build + Nginx static serving
│ ├── Dockerfile.tools # Full monorepo for seeds & scripts
│ └── nginx.conf # Nginx config for web container
├── curl-test-scripts # Manual API test scripts (23 shell scripts)
├── .github/workflows
│ └── ci.yml # PR checks: lint, typecheck, test
├── .husky/pre-commit # Runs lint-staged on commit
├── biome.json # Biome config (tabs, double quotes, Tailwind directives)
├── vitest.config.ts # Root Vitest config (V8 coverage)
├── docker-compose.yml # Production compose (api, web, migrate, tools)
├── pnpm-workspace.yaml # Workspace boundaries (apps/*, packages/*, tools)
├── turbo.json # Build pipeline configuration
├── tsconfig.base.json # Shared TypeScript configuration
└── .env.example # Required environment variables templateCore Architectural Decisions
1. Strict Workspace Isolation
We utilize a strict .npmrc configuration to prevent "Phantom Dependencies." Packages cannot access dependencies they do not explicitly declare in their package.json.
Enforcement (.npmrc):
shamefully-hoist=false
strict-peer-dependencies=true
auto-install-peers=true
save-workspace-protocol=rolling
prefer-workspace-packages=true
engine-strict=true
node-linker=isolated2. Centralized Configuration (packages/config)
Environment variables are not read directly by apps.
- The
configpackage loads the root.envfile usingdotenv. - It validates all variables using Zod v4 schemas.
- If the environment is invalid, the application crashes immediately on startup (Fail Fast).
3. The Database Strategy (Prisma 7 + tsup)
We utilize Prisma 7.4 with the @prisma/adapter-pg driver adapter and raw pg for the connection layer.
The Challenge:
The prisma-client generator outputs raw TypeScript rather than pre-compiled JavaScript. Node.js cannot run these files directly, and standard tsc compilation struggles with internal Prisma imports lacking .js extensions in an ESM environment.
The Solution: Bundling with tsup
We treat the database package as a bundled library.
- Build Phase:
tsupcompilessrc/index.tsand bundles the generated Prisma Client code into a single, valid ESM file located atdist/index.js. - Development Phase: The
package.jsonpoints"types"to./src/index.ts. This allows IDEs andtscto see the live TypeScript source without needing a build step for IntelliSense. - Runtime: The
package.jsonpoints"main"and"exports"to./dist/index.js.
Configuration (packages/database/package.json):
{
"main": "./dist/index.js",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --clean"
}
}4. Logic Isolation
To support Offline-First capabilities, business logic is decoupled from data persistence.
- The Brain (
packages/custom-fields): Contains pure functions (Rule Engine, Formula Evaluator, Validators). Depends only on@remote-eaze/sharedand Zod — zero database dependencies. It runs in both Node.js and the Browser (iPad). - The Muscle (
apps/api): Handles Persistence (Prisma), Caching (Redis), and HTTP transport. It consumes "The Brain" to validate data before saving.
5. Feature-Module API Pattern
The API is structured as a collection of independent Fastify Plugins.
- Structure: Each module (e.g.,
custom-fields) has its ownroutes.ts,service.ts,repository.ts, andschemas.ts. - Dependency Injection: Services accept dependencies (like Redis) via constructor, ensuring testability.
- Validation: All I/O is validated using
fastify-type-provider-zod. - API Docs: Swagger and Swagger UI are wired via
@fastify/swaggerand@fastify/swagger-ui.
6. Standardized Observability
- Responses: All API responses follow a strict JSON Envelope (
{ data, meta, message }). - Errors: A Global Error Handler intercepts all exceptions (Zod, Prisma, AppErrors) and formats them into standard HTTP error responses.
- Logging: Structured JSON logging via Pino, with request context (
requestId).
7. Authentication
Authentication is handled by Better Auth, shared across the API, web client, and tools. The API provides the auth endpoints, and the web client and seed scripts consume the same auth library for session management and user creation.
8. Design System (packages/ui)
The UI package is a full design system built on:
- Primitives: Radix UI (accordion, dialog, dropdown, select, tabs, tooltip, etc.)
- Icons: HugeIcons (Lucide is not used in the main app)
- Styling: Tailwind CSS v4 + CVA (class-variance-authority) + Tailwind Merge
- Fonts: IBM Plex Mono, IBM Plex Serif, Inter (via Fontsource)
- Extras: cmdk (command palette), sonner (toasts), react-day-picker, input-otp
The package exports raw TypeScript (main and exports point to src/) — no build step required. The consuming app (apps/web) handles compilation.
Infrastructure
Docker
The project includes a full Docker Compose setup for production deployment:
| Service | Description |
|---|---|
api | Fastify backend (multi-stage build, minimal runtime image) |
web | Vite-built static files served by Nginx |
migrate | Runs prisma migrate deploy then exits |
tools | On-demand container for seeds and maintenance scripts (profile: tools) |
docker compose build # Build all images
docker compose up -d # Start api + web
docker compose run --rm migrate # Run migrations
docker compose --profile tools run --rm tools pnpm seed:system-admin # Run seedsCI/CD
GitHub Actions runs on every PR to master:
- Install dependencies (
pnpm install --frozen-lockfile) - Generate Prisma client
- Generate TanStack Router route tree
- Lint (Biome)
- Typecheck (tsc)
- Test (Vitest)
Code Quality
- Pre-commit: Husky runs
lint-stagedwhich appliesbiome check --writeto staged.ts/.tsxfiles. - Biome config: Tab indentation, double quotes, organized imports, Tailwind CSS directive support.
- Vitest: Root config with V8 coverage provider, excludes
node_modules,dist, test files, and Prisma generated code.
Development Workflow
Prerequisites
- Node.js v22.15.0+
- pnpm v10.30.2+
- PostgreSQL
- Redis
Setup
# 1. Install dependencies from root directory
pnpm install
# 2. Generate Prisma Client (Required before dev)
pnpm db:generate
# 3. Start Development Server
pnpm devDatabase Management
All database commands should be run from the root, handled by Turbo or pnpm filtering.
# Apply migrations
pnpm db:migrate:dev
# Open Prisma Studio
pnpm db:studioNever use db:push
db:push causes schema drift. Always use the migration workflow (db:migrate:dev).
Available Scripts
| Command | Description |
|---|---|
pnpm dev | Start all apps in development mode |
pnpm build | Build all packages and apps in topological order |
pnpm start | Run compiled apps from dist/ |
pnpm lint | Run Biome linting across all packages |
pnpm lint:fix | Auto-fix lint issues (biome check --write) |
pnpm typecheck | Run TypeScript type checking |
pnpm test | Run Vitest across all packages |
pnpm test:watch | Run Vitest in watch mode |
pnpm db:generate | Generate Prisma client |
pnpm db:migrate:dev | Create/apply database migrations |
pnpm db:studio | Open Prisma Studio |
pnpm clean | Remove all build artifacts and node_modules |
pnpm seed:system-admin | Seed the system admin user |
pnpm seed:iso | Seed ISO reference data (countries, currencies, languages) |
pnpm cleanup:test-data | Remove test data from the database |
Production Build
We use Turbo to orchestrate the build order. It ensures dependent packages (config, database, custom-fields, shared) are built before the apps. The build task depends on ^build (all upstream packages) and db:generate (Prisma client generation).
# Builds all packages and apps in topological order
pnpm build
# Runs the compiled API from the dist/ folder
pnpm start