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;
Green = ships today (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-ui unified import.
SSR contract
Every file exports island: true | false | "subtree" so the build decides what ships to the client. No top-level window/document/process.
Theming
Tailwind v4 + CSS variables in app/styles/tokens.css; dark mode via .dark on <html>, no JS theming dep. Theme presets via --theme neutral|slate|stone|zinc.
Boundary
CI-safe and Bun-native: refuses NODE_ENV=production, never runs install, edits package.json and tells the user to bun 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-form role).
  • Schema validation already in reach — zod is 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 PattiesContext thin — do not grow it into a Next.js-style request abstraction (web-standards-boundary rule).
  • Validation is a plain zod call 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 + an island flag, 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.

Patties design specifications · a design exploration · User docs