Frontend & UI stack
The framework's frontend answer is React 19 + the patties-ui copy-in catalog — and that is the right and sufficient primitive layer. The open question is never "which frontend framework"; it's which three layers sit on top of the primitives, and where each one lives relative to patties. This page answers that.
Status: Draft — intended design only
No code exists for the forms/mutations primitive or the composed-blocks tier yet. This page records the decision and the layering; the patties-ui primitive catalog (60 components) already ships.
React 19 + patties-ui is the right primitive layer
"Is React & shadcn-alike enough?" — for the primitive layer, yes. Swapping the frontend framework is not a frontend choice; it is a different framework. Three reasons it stays React.
Patties is committed to React 19
SSR is renderToReadableStream; the island runtime, the web-standards boundary, and the whole patties-ui catalog are built on it. Svelte/Solid/Vue would mean rewriting all three.
Copy-in is the correct model
shadcn-style stamping (no versioned UI package) lets each module own and diverge its components. That is exactly what a modular monolith wants — no shared-package coupling across module boundaries.
SSR-first, island by opt-in
island: true | false | "subtree" means most of a back-office/CRUD monolith renders static and ships zero JS. That beats a default-everything-hydrates SPA framework for this app shape.
The framework + primitive layer is not the problem
React 19 + radix-ui primitives + Tailwind v4 tokens is a modern, defensible stack. "Enough?" is the wrong question for the framework; it's the right question for the layers above the primitives.
A UI is four layers — a catalog is only the first
A copy-in catalog (buttons, dialogs, inputs) is the primitive layer. A real modular-monolith app needs three more layers that shadcn-style catalogs deliberately do not ship. The friction is here — not in the choice of React.
flowchart TB
APP(["Modular-monolith app
(back-office · CRUD · dashboards)"])
L4["④ Client data cache
optimistic updates · revalidation"]
L3["③ Composed blocks
DataTable · dashboard shell · command-k · auth screens"]
L2["② Forms & mutations
form state · zod validation · server actions"]
L1["① Primitives
patties-ui — 60 components, radix + Tailwind v4"]
APP --> L4 --> L3 --> L2 --> L1
classDef have fill:#f0fdf4,stroke:#16a34a,color:#14532d,stroke-width:2px;
classDef gap fill:#fef9c3,stroke:#ca8a04,color:#713f12,stroke-width:2px;
class L1 have;
class L2,L3,L4 gap;
patties-ui). Yellow = the three layers still to design. A DataTable, not a styled <Input>, is what a monolith lives or dies on.① Primitives — ships today
This layer is done. patties-ui is the source of truth (src/registry.ts), all 60 components are status: "completed", and patties add <component> stamps verbatim source into app/components/ui/.
- Done Surface
- 60 copy-in components — buttons, inputs, dialogs, selects, popovers, etc. — React 19 style (no
forwardRef, refs as plain props),radix-uiunified import. - SSR contract
- Every file exports
island: true | false | "subtree"so the build decides what ships to the client. No top-levelwindow/document/process. - Theming
- Tailwind v4 + CSS variables in
app/styles/tokens.css; dark mode via.darkon<html>, no JS theming dep. Theme presets via--theme neutral|slate|stone|zinc. - Boundary
- CI-safe and Bun-native: refuses
NODE_ENV=production, never runsinstall, editspackage.jsonand tells the user tobun install.
Note
See the UI catalog rule (.claude/rules/ui-catalog.md) for the full patties-ui contract. Nothing in this layer needs new design — it is the foundation the next three build on.
② Forms & mutations — the biggest core gap
shadcn gives you a styled <Input>, not form state, validation wiring, or a server-mutation convention. This layer touches PattiesContext, server actions, and the standards boundary — so it almost certainly belongs in patties core, not the catalog.
What's missing
- Form state + binding (the
react-hook-formrole). - Schema validation already in reach —
zodis in the toolchain. - A server-action / mutation primitive: submit → validate → run handler → return typed result or field errors, over a standard
Request/Response. - Progressive enhancement: the form posts and works before the island hydrates.
Design constraints
- Keep
PattiesContextthin — do not grow it into a Next.js-style request abstraction (web-standards-boundary rule). - Validation is a plain
zodcall in the handler or a shared schema, not framework magic. - The mutation result shape must round-trip an island and an SSR no-JS submit identically.
Lives in core
A patties-native form-action primitive (zod-validated, server-action backed, island-and-no-JS symmetric) is the highest-value missing piece. It is a framework concern because it is the seam between a standard Request and the React island.
③ Composed blocks — a patties-ui higher tier
Primitives compose into patterns. shadcn ships these as blocks/examples, not registry components — but a monolith needs them as first-class, stampable entries built on the 60 primitives.
The patterns that matter
- DataTable — sorting, filtering, pagination, selection. The single most load-bearing block in a back-office app.
- Dashboard shell — sidebar + topbar + content slots, responsive.
- Command palette (⌘K) — cross-module navigation.
- Auth screens — login, register, multi-step flows.
How it fits the catalog
- New registry entries with
status+ anislandflag, same schema (ComponentEntrySchema). - Stamped by the same
patties add— diff-aware, verbatim, never executed. - Built from primitives already in the catalog, so a stamp pulls its deps.
- Mostly
"subtree"islands: a static shell with a nested interactive island (e.g. the DataTable body).
Lives in patties-ui
These are composition, not framework surface — a "blocks" tier in the registry, above the primitives. A module stamps a DataTable and adapts it locally, exactly the copy-in model the primitives already use.
④ Client data cache — opt-in, per island
For the island parts that need optimistic updates and revalidation, a client cache (TanStack Query is the standard) helps. But in a heavily-SSR patties app, most of this is pushed to the server — which is the better monolith story, not a worse one.
- Default
- Server-rendered Read data on the server, render it into the page. No client cache needed for the static majority of a CRUD monolith.
- Interactive islands
- Opt-in An island that needs optimistic mutation or background revalidation pulls in TanStack Query locally — it is not framework-mandated and not shipped to static pages.
- Mutations
- Prefer the core form-action primitive (layer ②) + revalidate-on-success over a bespoke client mutation cache wherever the SSR round-trip is fast enough.
Not framework-mandated
The client cache is an island-level dependency, opt-in where interactivity demands it. Keeping it out of the framework default preserves the zero-JS-by-default story for the bulk of the app.
Where each layer lives — relative to patties
The decision is not "what to add" but "where it belongs." Three different homes, one per layer above the primitives.
| Layer | Home | Status | Why there |
|---|---|---|---|
| ① Primitives | patties-ui registry |
Ships | Copy-in catalog, 60 components completed. |
| ② Forms & mutations | patties core | Draft | Seam between standard Request, server actions, and the island — a framework concern. |
| ③ Composed blocks | patties-ui "blocks" tier |
Draft | Composition over primitives; stamped, adapted per-module. Not framework surface. |
| ④ Client data cache | Per-island dependency | Opt-in | Interactivity-only; keeping it out of the default preserves zero-JS SSR. |
Decision
Do not change frontend frameworks
React 19 + patties-ui is the correct, sufficient primitive layer for a modular monolith. The work is in the three layers above it:
- Spec a forms / server-action primitive in patties core (zod-validated, island-and-no-JS symmetric).
- Add a "blocks" tier to
patties-ui— DataTable, dashboard shell, command-k, auth screens — as registry entries built on the 60 primitives. - Leave the client data cache opt-in, per island.
Per the repo's RFC → spec workflow, each of the two "draft" rows above becomes its own new spec rather than an edit to an archived one. This page is the framing they share.