# ADR-0037: Drop Refine framework; migrate to TanStack Query + AntD + react-router

**Date**: 2026-04-20 (proposed), 2026-04-21 (accepted)
**Status**: accepted — all five waves (A–E) shipped on `m21/drop-refine` 2026-04-21. Zero `@refinedev/*` imports remain under `src/`, `tests/`, or `package.json`; production bundle dropped from ~2.2 MB to ~1.6 MB (gzip: 700 → 507 kB). See the "Outcome" section below.
**Deciders**: shuki + Claude
**Related**: ADR-0034 (M20 Kratos identity — supersedes Refine's `authProvider` role), ADR-0036 (M16 Hydra — unaffected by this change), memory `project_plan_panel_ui_migration` (parked 2026-04-17, pre-Kratos)

## Context

The panel's frontend was scaffolded on **Refine** (`@refinedev/core` + `@refinedev/antd` + `@refinedev/react-router`) in M1–M5 when we were rapidly building CRUD pages. Refine bundled an `authProvider`, `dataProvider`, `routerProvider`, and a set of opinionated wrapper components (`<Create>`, `<Edit>`, `<Show>`, `<List>`, `<ThemedLayoutV2>`) that gave us fast-to-scaffold admin pages.

Over the last day the operator attempted multiple UI refactors — AntD 5 → 6 upgrade, MUI-flavored theme, stock-AntD revert, badge defaults, search-input swap — each aimed at getting to a clean, stock-AntD look. The whack-a-mole result exposed that the visual character the operator wants to move away from (auto-generated headings, breadcrumbs, back-arrow buttons, card containers, sticky save bars) is **injected by Refine's page wrappers**, not by our call-site styling. No amount of caller-level tweaking reaches it.

At the same time, M20 (Kratos) and M16 (Hydra) landed this week. Kratos now owns identity, MFA, session management, and password recovery end-to-end via self-service browser flows; panel-api is a thin cookie validator. The Refine `authProvider` has shrunk to a ~100-line adapter around Kratos whoami/logout calls — it's no longer load-bearing. Dropping it removes indirection, not functionality.

The prior "panel-ui migration" attempt (memory entry 2026-04-17, 14:1 LOC ratio) was parked as a yak-shave **before Kratos landed**, when replacing Refine also meant replacing the entire authentication stack. That constraint is gone.

## Decision

Remove all four `@refinedev/*` packages (`core`, `antd`, `react-router`, `simple-rest`) from the panel-ui. Replace each capability with a mainstream 2026 stack:

- `authProvider` → React `AuthContext` + `useAuth()` (already wraps Kratos calls)
- `dataProvider` + `useList`/`useOne`/`useCreate`/`useUpdate`/`useDelete` → **TanStack Query** (`@tanstack/react-query`) + existing `apiClient.ts`
- `useForm` → AntD built-in `Form.useForm()` + manual `onFinish` → `mutateAsync` → `navigate`
- `useTable` → **hand-rolled 100-line `useTableURL`** hook using `useSearchParams` for URL round-trip
- `useSelect` → **hand-rolled 30-line `useSelectQuery`** hook
- `routerProvider` + `resources` array → react-router v7 directly, with sidebar menu from a static `src/nav.ts` config
- `<Authenticated>` / `<CanAccess>` → `<RequireAuth>` / `<RequireAdmin>` components reading `useAuth()`
- `<Create>` / `<Edit>` / `<Show>` / `<List>` / `<ThemedLayoutV2>` wrappers → **deleted**; pages compose AntD primitives directly; a shared `<FormPageFooter>` provides sticky save-bar UX for long forms that want it

Work tracked as **M21 — Drop Refine** in `plans/m21-drop-refine.md` across 5 waves (A: foundation; B: shell; C: admin pages; D: user pages; E: cleanup). Wt-b is the implementing session.

## Alternatives Considered

### Alternative 1: Keep Refine, strip wrappers case-by-case (option (a) in the triage conversation)
- **Pros**: no framework churn; keeps useTable/useForm hooks as-is.
- **Cons**: the chrome the operator wants removed (auto headings, breadcrumbs, card wrapping) is generated by the wrappers themselves. Stripping at call sites doesn't reach it. This is the path the day-long refactor chain was on; it failed.
- **Why not**: empirically demonstrated not to work. Rejected by the operator after whack-a-mole exhaustion.

### Alternative 2: Drop Refine's wrappers, keep its hooks (option (b))
- **Pros**: minimal churn (~20 files); keeps Refine's data layer, auth provider, URL-synced useTable.
- **Cons**: still carries four `@refinedev/*` packages in the dependency tree. Any future Refine release, deprecation, or CVE remains our problem. Keeps the `<Refine>` root component and `resources` array — both of which are indirection without payoff.
- **Why not**: half-measures cost the same day count as the full migration (1–2 days), because the test-selector updates and page rewrites are the bulk of the work either way. If we're already touching every page, the incremental cost of also swapping the data layer is small, and the upside (no more Refine dep) is permanent.

### Alternative 3: Drop Refine AND Kratos, go back to hand-rolled auth
- **Pros**: fewer moving parts.
- **Cons**: catastrophic — M16 Hydra just shipped and requires Kratos as its identity provider via login-consent delegation. Rebuilding the identity side of Hydra ourselves is significantly more work than the M20 migration was. Also reintroduces ~5,900 lines of JWT + TOTP + refresh-token + password-recovery code we deleted in the M20 cleanup. MFA goes away. Self-service flows (settings, recovery) go away.
- **Why not**: Kratos's cost is already paid and M16 now rides on it. Confusing frustration with Refine's chrome for regret about Kratos is a category error — they are unrelated layers.

### Alternative 4: Drop Refine and rewrite in a different UI framework (Tailwind + shadcn, Chakra, Mantine, etc.)
- **Pros**: clean-sheet UI.
- **Cons**: multi-week rewrite; re-teaches every E2E selector; re-implements every form validation; no proven ROI for a single-operator panel.
- **Why not**: AntD works, it's just shrouded by Refine. Drop the shroud, keep the library.

## Consequences

### Positive
- The panel's UI becomes plain AntD: easier to reason about, easier to style, no magic between `<Form.Item>` and the page heading.
- Removing four packages (Refine core + Refine/antd + Refine/react-router + Refine/simple-rest) shrinks the main bundle by an estimated ~100–150kB.
- TanStack Query is a better data layer than Refine's: smarter cache keys, optimistic updates, mutation chaining, query devtools.
- No more "which hook replaces which hook" indirection — the stack is React + AntD + TanStack Query + react-router, documented by three projects instead of four-plus-Refine-conventions.
- Simpler onboarding: a new contributor reads stock AntD + TanStack Query docs instead of learning Refine's opinions on top of those.
- Easier to fork / adapt / sell as open-source: fewer upstream dependencies on opinionated framework evolution.

### Negative
- **3–5 careful days of work** touching ~26 files and ~53 call sites, with full-suite verification after each wave.
- **Sticky save-bar regression** on long forms — mitigated by shared `<FormPageFooter>` opt-in, but a few pages will lose the affordance if not explicitly reinstated.
- **Some E2E selectors break** — roughly 5–10 Playwright assertions expect Refine-generated headings/breadcrumbs. Each one updates to assert on the new explicit heading or removes the assertion if obsolete.
- **URL-synced table state becomes our code to maintain** — 100 lines of `useTableURL` replacing Refine's battle-tested implementation. Bugs here regress bookmarkability; covered by vitest unit tests + targeted E2E.

### Risks
- **R1** — Refine's `useInvalidate` usage across the codebase may encode cache-invalidation assumptions we miss when migrating to TanStack Query's key-based invalidation. Mitigation: every callsite of `useInvalidate` gets grep'd and individually translated to `queryClient.invalidateQueries({ queryKey: [...] })`; no batch replacement.
- **R2** — `useNotification` call-sites currently call Refine's abstraction; migrating to AntD's `message`/`notification` directly is trivial per call, but the notification-provider context chain in AntD requires `App.useApp()` or the `notification` singleton. Mitigation: use the singleton for simplicity; no global provider change needed.
- **R3** — `<Authenticated>` wraps routes at the route level; if its replacement `<RequireAuth>` introduces a flash-of-unauthenticated-UI on cold load (because whoami is async), users see a brief layout before the redirect. Mitigation: show a full-page `<Spin>` while `isLoading`.
- **R4** — `@refinedev/simple-rest` is listed as a dep but the dataProvider.ts is custom. Removing it shouldn't break anything, but a blanket `npm install` after `package.json` edit will regenerate the lockfile; coordinate with any in-flight npm work. Mitigation: Wave E runs `npm install` (not `npm ci`) and verifies `npm run build` before push.
- **R5** — wt-a may still ship Wave F (Automation API) during M21 execution; their code lands backend + some apps UI. Mitigation: M21 blueprint calls out `shells/*/applications/*` as wt-a's territory; wt-b coordinates before touching those specific files.

## Rollback

`git revert` the M21 merge commits. No schema changes, no config changes, no new services or daemons. `package-lock.json` restores the Refine packages on next `npm install`. Rollback is fast (minutes) and non-destructive.

## Scope fences (not in M21)

- Any change to Kratos or the Kratos browser-flow handling (`Login.tsx`, `kratos.ts`, authProvider behavior beyond adapting the interface).
- Rewriting `Hydra` consent page — it was just shipped in M16 Wave C (`c5b030e`). It does use Refine wrappers; treated as a normal page in Wave C of M21.
- Replacing AntD with another UI library.
- Backend changes — this is frontend-only.
- Updating the `docs/BLUEPRINT.md` for milestones other than §M21.

## Acceptance criteria

- `grep -r "@refinedev" panel-ui/src panel-ui/tests` returns 0 hits.
- `grep -r "refinedev" panel-ui/package.json` returns 0 hits.
- `make test-all` green (Go vet + Go test -race + vitest + Playwright 18+/18+).
- Manual smoke: admin login → users list → create user → edit user → delete user → navigate to every top-level admin page; same for user-shell.
- Bookmark test: navigate to `/jabali-admin/users?page=2&q=admin&sort=email&order=asc`, refresh, state preserved.
- Main bundle size reduced (target: ≥50kB gzipped drop, measurable via `vite build` size report).
- BLUEPRINT §M21 → SHIPPED, ADR-0037 → accepted.

---

## Outcome (2026-04-21)

All five waves shipped on `m21/drop-refine` across one day (2026-04-20 → 2026-04-21):

- **Wave A** (foundation) — `src/query.ts`, `src/hooks/{useQueries,useTableURL,useSelectQuery}.ts`, `src/auth/{AuthContext,RequireAuth,RequireAdmin,RequireUser}.tsx`. Eight unit tests.
- **Wave B** (shell) — `src/App.tsx` rewritten to `QueryClientProvider > AuthProvider > BrowserRouter > ConfigProvider > Routes`. `AdminLayout`/`UserLayout` reduced to plain AntD `<Layout>` + `<Sider>` + `<Header>` + `<Content>` + `<Footer>`. Single source of menu items at `src/nav.ts`.
- **Wave C** (admin pages) — users, packages, domains, DNS, SSL, settings, PHP pools, databases, database-users all off Refine. Fixed a Wave A wire-contract bug where `useQueries.ts` was shaped for `{items, total}` but panel-api actually returns `{data, total, page, page_size}`. The four users-spec E2E failures that had been red on `main` since pre-Wave A cleared on this commit.
- **Wave D** (user pages + shared chrome) — domains, databases (with Quick Setup + phpMyAdmin SSO), DNS, applications (with transitional-state polling). Shared `Domain*` buttons + `dns/DNSRecordsPage` moved from `useInvalidate`/`useNotification` to `useQueryClient` + AntD `notification`. Dead files (`shellSider.tsx`, `RoleGate.tsx`) deleted.
- **Wave E** (cleanup) — `@refinedev/{core,antd,react-router,simple-rest}` removed from `package.json`; `@ant-design/icons` promoted to direct dep; `authProvider.ts`, `dataProvider.ts`, `searchableTableUtils.ts` deleted; `SearchableTable.tsx` trimmed to its string-`q` variant; Login/Consent tests dropped their `<Refine>` wrapper; `main.tsx`'s `@refinedev/antd/dist/reset.css` → `antd/dist/reset.css`; `overrides: { antd: "$antd" }` removed (no longer needed without Refine's `antd@5` peer).

### Measured results

- `grep -r "@refinedev" panel-ui/src panel-ui/tests panel-ui/package.json`: 0 hits ✓
- `npx tsc -b`: clean ✓
- `npm test`: 28/28 vitest ✓
- `npm run build`: clean ✓; **production JS 2,192 kB → 1,586 kB (−606 kB / −27.6%)**, gzip 700.74 kB → 507.37 kB (−193 kB / −27.6%). Beats the ≥50 kB gzipped target by ~4×.
- `npm run test:e2e`: 22/22 ✓
- `node_modules/` installed packages: 358 → 239 (−119).

### Deviations from the blueprint

- The Wave C "Refine hooks return `{data, total, page, page_size}`" detail was not reflected in the Wave A hook signature until a fix-in-flight folded into Wave C.1. Logged as a lesson for future wire-contract work — look at `panel-api/internal/api/*.go` first, not the blueprint's assumed envelope.
- `Login.tsx` and `Consent.tsx` were scope-fenced from M21 edits, but Wave E had to remove the `<Refine>` test-stub wrapper from their `*.test.tsx` files and inline a 3-line `homeForRole` helper in `Login.tsx` once `authProvider.ts` was deleted. These were unavoidable consequences of the cleanup, not functional changes to the auth flow.
- `admin/applications/AdminApplicationList.tsx` was rewritten in Wave E despite being wt-a's territory — keeping its `useTable` usage would have blocked `@refinedev/*` removal. The rewrite is read-only view code; no behavioural change.

