| name | project-mtp-stack | ||
|---|---|---|---|
| description | Confirmed stack for Multi-Tenant Platform — ASP.NET Core Web API backend + React 19 + Radix Primitives + CSS Modules + native CSS Grid/container queries + TanStack Table/Virtual + Zod/RHF/TanStack Query/Zustand frontend. Replaces the MUI-based plan from ADR-0003. | ||
| metadata |
|
For the multi-tenant-platform repo at D:\Projects\mtp\mtp-docs:
Authoritative ADR: adr/0007-ui-stack-radix-cssmodules.html (supersedes ADR-0003).
Why this stack: User wants Tally-like desktop feel in the browser — sub-50ms screen switches, keystroke-driven navigation, dense data over thousands of rows. Two non-negotiables locked 2026-05-20:
- No runtime CSS-in-JS.
- Minimal framework intrusion — primitives are headless; the team owns its DOM and CSS.
How to apply:
- ASP.NET Core Web API (latest stable — .NET 10 at time of writing)
- MVC controllers (
ControllerBase+[ApiController]+[HttpGet]/[HttpPost]). No Minimal APIs (per [[feedback-no-minimal-apis]]). - Traditional layered / service-oriented (per [[feedback-no-cqrs-no-mediatr]]).
- ErrorOr for service results. FluentValidation for shape validation.
- EF Core 9 (Npgsql) + Dapper for reads.
- PostgreSQL 17+ with ltree (per [[project-mtp-postgres-ltree]]).
- Per-request connection-string resolution (per [[project-mtp-tenancy-model]]).
- React 19+ + TypeScript 5.9+ — strict mode on.
- Radix UI Primitives (
@radix-ui/react-*) — headless behaviour, ARIA, keyboard, focus, portal. Opt-in per primitive (Dialog, Popover, DropdownMenu, Tooltip, Select, Tabs, Toast, ScrollArea, AlertDialog, ContextMenu). Use the primitives package only — not the styled@radix-ui/themespackage. - CSS Modules + PostCSS + CSS custom properties — compile-time CSS, scoped class names, zero runtime serialization. One
.module.cssnext to each component file. - Native CSS Grid + Container Queries — no grid library. In-house
Grid/Stack/Cluster/SplitPaneprimitives (~30 lines CSS each).@containerfor component-level responsiveness; media queries only for app-shell breakpoints. - TanStack Table + TanStack Virtual — headless table state + virtualization. Wrap in own
DataGridcomponent that owns sticky headers, column resize, keyboard nav. - lucide-react — icon set; matches the inline SVG style already used in
mockups/*.html(24×24, stroke-based,currentColor). - Zod — schema validation; single source of truth for client types + runtime validation; pairs with RHF via
@hookform/resolvers/zod. - React Hook Form — form state, low re-render; resolver = Zod schemas.
- TanStack Query (v5+) — server state, caching, revalidation, ETag-aware.
- Zustand — small client/UI stores (auth, current client code, current locale, ephemeral UI flags).
- react-hotkeys-hook — F-key / Alt-key shortcuts for Tally-style navigation.
- i18next +
react-i18next(per page 08). - Vite (latest) — build tool.
- React Router (latest, data mode) — routing.
- HTTP: native
fetch+ thin wrapper that attaches JWT + client-code header. No axios.
- Tokens defined as CSS custom properties in
tokens.css(--color-primary,--space-2,--font-sm, etc.). - Tenant skin = toggle
data-themeattribute on<html>. CSS variables resolve down the cascade. No JS re-render path. - Dark mode = same mechanism, different variable values.
- Server-side validation = FluentValidation. Authoritative.
- Client-side validation = Zod. Optimistic; mirrors server rules for UX. Never sole gatekeeper.
- Shape contract flows: OpenAPI schema → generated TypeScript types → Zod schemas authored alongside types.
- Result envelope server-side:
Result<T>(per page 06) → JSON. Client: TanStack Query mutations/queries unwrap intoResult<T>-typed responses; UI dispatches byIssueKind(Advisory/Confirmable/Blocking) per page 05. - Form binding: RHF on plain
<input>/<select>. For overlays/menus/dialogs, RHF state + Radix primitives wrapped in own components. - Forms render off platform UI schema (per page 07): generic
<FieldRenderer>switches onFieldKey→ in-house input components.
- No runtime CSS-in-JS of any kind (rules out any styling library that serializes styles at render time).
- No compile-time CSS-in-JS either — CSS Modules is sufficient.
- No utility-class CSS framework — style stays in
.cssModule files, not in JSX. - No styled component libraries — primitives must be headless.
- No Redux / Redux Toolkit — Zustand covers it.
- No Formik — RHF.
- No Yup — Zod.
- No SWR — TanStack Query.
- No axios —
fetch+ thin wrapper. - No global-CSS framework (Bootstrap-style) — class-name leaks.
- No CRA / Next.js / Remix — Vite SPA; no SSR/edge needs for the admin app.
- TypeScript strict mode on:
noUncheckedIndexedAccess,noImplicitOverride,noFallthroughCasesInSwitch. - Every component:
Component.tsx+ at most oneComponent.module.css. - All colour/spacing/font tokens via CSS variables in
tokens.css. No hardcoded hex/rem in component CSS. - Tenant theme switch =
data-themeattribute on<html>. No JS re-render. - Radix primitives wrapped in own components (
Dialog,Popover, etc.); never imported raw into feature code. - Zod schemas authored next to types; types via
z.infer— never duplicated. - One
QueryClientper app. TanStack Query keys:[entity, ...filters]tuple. - All forms wired through RHF + Zod; no raw
onSubmithandlers in feature code. - Cross-component state in Zustand or TanStack Query cache. No prop drilling beyond 2 levels.
- Container queries preferred over media queries for component responsiveness.
- No new dep adds without an ADR if > 50 KB gz or duplicates an existing concern.
Documented today: feature-folder pattern in mtp-web — src/features/<feature>/manifest.ts + pages/, shell at @/app/feature-registry.
Not yet locked: location of primitives/, components/, lib/, hooks/, styles/tokens.css. Will be settled in a follow-up ADR when mtp-web repo is initialized.
- [[project-mtp-postgres-ltree]] (database)
- [[project-mtp-tenancy-model]] (isolation)
- [[project-mtp-repo-topology]] (repo split + feature-folder pattern)
- [[feedback-no-cqrs-no-mediatr]] + [[feedback-no-minimal-apis]] (backend style)