Architecture Decisions
State Management: Zustand over Redux
| Factor | Decision |
|---|---|
| Stores | 4 small, focused stores vs. one large Redux store |
| Persistence | Each store uses different storage: IndexedDB (pipeline), localStorage (workflow), sessionStorage (access mgmt) — Zustand middleware is simpler |
| Size | Each store < 500 lines, no complex reducer logic |
| Server state | React Query handles all server state, Zustand only manages local UI state |
Visual Designers: XYFlow (@xyflow/react)
| Factor | Decision |
|---|---|
| Usage | Pipeline designer + Workflow designer (shared DesignerCanvas) |
| Features | Canvas with zoom/pan, node/edge rendering, connection handling, drag-and-drop |
| Custom nodes | OperatorNode, TesterNode, 11 workflow node types |
| Layout | @dagrejs/dagre for auto-layout (DAG algorithms) |
UI Primitives: React Aria Components
| Factor | Decision |
|---|---|
| Design system | @dtx/ui built on Untitled UI, which uses React Aria |
| Accessibility | ARIA roles, keyboard navigation, focus management out of the box |
| Components | Select, ComboBox, Modal, Dialog, Button, Table, Tabs, DatePicker |
| Tailwind | tailwindcss-react-aria-components for state variant integration |
Monorepo: pnpm Workspaces
| Factor | Decision |
|---|---|
| Packages | apps/portal, packages/ui, packages/utils |
| Versioning | Catalog in pnpm-workspace.yaml centralizes all dependency versions |
| Linking | @dtx/ui is source-linked (no build step, direct TypeScript imports) |
| Isolation | pnpm strict dependency resolution prevents phantom dependencies |
Other Decisions
| Decision | Rationale |
|---|---|
| Tailwind CSS 4 (native Vite plugin) | No PostCSS config needed, faster builds, @theme block in CSS |
| Keycloak for auth | Enterprise OAuth2/OIDC with multi-tenancy, realm-based isolation |
| Monaco Editor | Full IDE-like SQL/expression editing in pipeline operators |
| Recharts over D3 | Simpler API for standard charts; D3 available for custom visualizations |
| React Hook Form + Zod | Type-safe validation, multi-step wizard support via useFormContext() |
| Sonner for toasts | Lightweight, Tailwind integration |
| Manual Vite chunks | Separate chunks for react, query, monaco, d3, react-flow — optimized loading |
| IndexedDB for pipelines | Pipeline state (nodes, edges, configs) exceeds localStorage 5MB limit |
| Singleton services | All API services use getInstance() — single instance per service |
| snake_case → camelCase | Backend uses snake_case; parseApiResponse() auto-converts all responses |