Skip to content

feat: admin framework adapter pattern and tanstack support#16139

Draft
r1tsuu wants to merge 63 commits intomainfrom
experiment/framework-adapter-pattern
Draft

feat: admin framework adapter pattern and tanstack support#16139
r1tsuu wants to merge 63 commits intomainfrom
experiment/framework-adapter-pattern

Conversation

@r1tsuu
Copy link
Copy Markdown
Member

@r1tsuu r1tsuu commented Apr 2, 2026

This is an experiment for now

@jmbockhorst
Copy link
Copy Markdown
Contributor

Hey, I am excited about this work! I had experimented with a similar approach a few months ago. I'm sure you might be aware that TanStack Start is planning to add RSC support soon (there is an draft blog post here, not sure how up to date it is with their current plans). I was wondering if this would make a difference in the approach to the framework adapter pattern, and if RSCs in Start make this significantly easier. If you haven't already, it might be good to have some communication with the TanStack team about this. Thanks for your work on this!

@r1tsuu
Copy link
Copy Markdown
Member Author

r1tsuu commented Apr 3, 2026

Hey, I am excited about this work! I had experimented with a similar approach a few months ago. I'm sure you might be aware that TanStack Start is planning to add RSC support soon (there is an draft blog post here, not sure how up to date it is with their current plans). I was wondering if this would make a difference in the approach to the framework adapter pattern, and if RSCs in Start make this significantly easier. If you haven't already, it might be good to have some communication with the TanStack team about this. Thanks for your work on this!

I'm aware, currently I want to see if it is possible so we don't rely on whether a framework supports RSC or not (while still maintaining 100% the same approach in Next.js). This is a bit more complex indeed but would allow room for any React framework (or even a custom one on top of Vite), not just Next/Tanstack. In this case - when Tanstack will add RSC support and if we want to use it - the only place we'd have modify is the adapter itself.

r1tsuu added 27 commits April 4, 2026 00:39
Add RouterAdapter, ServerAdapter, ComponentRenderer, and DevReloadStrategy
type contracts in packages/payload/src/admin/adapters.ts. These types form
the foundation for decoupling the admin panel from Next.js.
…imports

- Create RouterAdapter pattern: adapter is a React component that wraps
  children and populates RouterAdapterContext with framework-specific values
- Replace all 41 files importing from next/navigation.js, next/link.js,
  and next/dist/* with framework-agnostic RouterAdapter equivalents
- Replace AppRouterInstance type with RouterAdapterRouter from payload
- Replace ReadonlyRequestCookies with CookieStore from payload
- Replace LinkProps from next/link with LinkAdapterProps from payload
- Remove next from packages/ui peerDependencies
- Wire RouterAdapter component into RootProvider
- Export RouterAdapterContext from client entrypoint
- Create NextRouterAdapter component that calls Next.js hooks (useRouter,
  usePathname, useSearchParams, useParams) and populates the framework-agnostic
  RouterAdapterContext
- Wire NextRouterAdapter into RootLayout as the RouterAdapter prop
- Export NextRouterAdapter from @payloadcms/next/client
Move pure routing utilities from packages/next/src/views/Root/ to
packages/ui/src/utilities/routeResolution/:
- isPathMatchingRoute, getDocumentViewInfo, attachViewActions
- getCustomViewByKey, getCustomViewByRoute
- Shared ViewFromConfig type

Original files in packages/next re-export from @payloadcms/ui for
backward compatibility. getRouteData.ts updated to import from shared.
Move framework-agnostic presentational components from packages/next:
- MinimalTemplate (template + styles) → packages/ui/src/templates/Minimal/
- FormHeader (element + styles) → packages/ui/src/elements/FormHeader/

Original locations in packages/next now re-export for backward compat.
Create serverFunctionRegistry in packages/ui with framework-agnostic
handlers (form-state, table-state, copy-data-from-locale, etc.).
packages/next handleServerFunctions now spreads the shared registry
and adds RSC-specific handlers (render-document, render-list, etc.).
Create a client-only component renderer that treats all components as
client components and never passes serverProps. This is the alternative
to RenderServerComponent for frameworks without RSC support.
Add candidateDirectories parameter to resolveImportMapFilePath, allowing
framework adapters to specify their own directory patterns instead of
defaulting to Next.js app/(payload) convention. The default behavior
is unchanged for backward compatibility.
Remove import of Metadata from 'next' in packages/payload config types.
Define AdminMeta type that covers the commonly-used metadata subset
(title, description, openGraph, icons, twitter, keywords). MetaConfig
now intersects with AdminMeta instead of Next.js Metadata.

The Next.js adapter can map AdminMeta to Next.js Metadata as needed.
Replace @next/env dependency with dotenv + dotenv-expand for
framework-agnostic .env file loading. The new implementation supports
the same file priority convention (.env.local, .env.development, etc.)
without requiring Next.js packages.
Replace hardcoded Next.js webpack-hmr WebSocket with a DevReloadStrategy
interface. getPayload() now accepts an optional devReloadStrategy parameter.
The default fallback preserves the current Next.js HMR behavior. Framework
adapters can provide their own strategy (e.g., Vite HMR for TanStack Start).
Replace ReadonlyRequestCookies from next/dist with CookieStore from
the framework adapter contract in getRequestLanguage.ts.
packages/payload now has zero imports from next/ or @next/.
Introduce PAYLOAD_FRAMEWORK env variable to control which framework
adapter the dev server starts with. Extract Next.js-specific startup
into test/adapters/nextDevServer.ts. The dev.ts script dispatches to
the appropriate adapter based on PAYLOAD_FRAMEWORK (defaults to 'next').

This enables future adapters (e.g., tanstack-start) to add their own
dev server module and be selected via PAYLOAD_FRAMEWORK=tanstack-start.
Thread a `renderComponent: ComponentRenderer` parameter through the
entire form state and table state pipelines instead of hardcoding
`RenderServerComponent` imports.

Files modified:
- renderField.tsx: accepts renderComponent param instead of importing directly
- buildColumnState/index.tsx, renderCell.tsx: accept renderComponent param
- renderTable.tsx, renderFilters: accept renderComponent param
- buildFormState.ts, buildTableState.ts: pass RenderServerComponent as default
- iterateFields.ts, addFieldStatePromise.ts: thread renderComponent through
- fieldSchemasToFormState/index.tsx: accept and forward renderComponent
- renderFieldServerFn.ts: pass RenderServerComponent explicitly
- richtext-lexical rscEntry.tsx, buildInitialState.ts: thread renderComponent

Non-RSC adapters can now pass RenderClientComponent instead.
Move framework-agnostic Nav, DocumentHeader, and Logo elements from
packages/next to packages/ui. Replace next/navigation hooks with
RouterAdapter hooks. Replace @payloadcms/ui barrel imports with
direct source imports. Leave re-exports in packages/next for backward
compatibility.
Move the Default template (Wrapper, NavHamburger) from packages/next
to packages/ui. Replace @payloadcms/ui barrel imports with direct
source imports. Leave re-exports in packages/next.
Move the following view helpers from packages/next to packages/ui:
- Version/RenderFieldsToDiff (entire directory, 22+ files)
- Version/fetchVersions.ts, VersionPillLabel/
- Versions/buildColumns.tsx, cells/, types.ts
- Dashboard/ (entire tree, 18+ files)
- Document/ helpers (getDocumentData, getDocumentPermissions, etc.)
- List/ helpers (handleGroupBy, renderListViewSlots, etc.)

All @payloadcms/ui imports converted to relative paths.
Re-exports left in packages/next for backward compatibility.
Remove outdated TODO comments in PerPage and Autosave components
that referenced next/navigation abstraction - these components
already use RouterAdapter or don't need navigation hooks at all.
Move the following auth-related view components to packages/ui:
- Login/LoginForm, Login/LoginField, Login styles
- ForgotPassword (full view + ForgotPasswordForm)
- ResetPassword (full view + ResetPasswordForm)
- CreateFirstUser (full view + client component)
- Verify (full view + client component)
- Logout (full view + LogoutClient)
- Unauthorized (full view)

All next/navigation imports switched to RouterAdapter.
All @payloadcms/ui barrel imports converted to relative paths.
Re-exports left in packages/next for backward compatibility.
Login entry point stays in packages/next (uses redirect()).
Move APIView, APIViewClient, RenderJSON, LocaleSelector and styles
from packages/next to packages/ui. Switch useSearchParams from
next/navigation to RouterAdapter. Convert @payloadcms/ui barrel
imports to direct relative paths. Re-exports left in packages/next.
Move AccountClient, Settings, LanguageSelector, ToggleTheme,
and ResetPreferences from packages/next to packages/ui.
Account entry point stays in packages/next (uses notFound()).
All @payloadcms/ui barrel imports converted to relative paths.
Move DefaultVersionView, Restore, SelectComparison, SelectLocales,
VersionDrawer, VersionDrawerCreatedAtCell, SelectedLocalesContext,
SetStepNav, and VersionsViewClient to packages/ui.

All next/navigation imports switched to RouterAdapter.
All @payloadcms/ui barrel imports converted to relative paths.
Re-exports created in packages/next for backward compatibility.
Also fixes missing B3 re-exports for VersionPillLabel, Versions
buildColumns/cells, and RenderFieldsToDiff.
Move NotFoundClient and styles to packages/ui. The NotFoundPage
entry point stays in packages/next (uses initReq, Metadata).
Re-export created in packages/next for backward compatibility.
r1tsuu added 23 commits April 4, 2026 15:11
…nstruction sites

Ensure every serverProps object across packages/next and packages/ui includes
renderComponent so import map components receive the adapter-injected renderer.
This allows custom server components to render other import map components
without importing a framework-specific renderer directly.
…er + client component

Split the only async component in packages/ui into framework-agnostic parts:
- getCollectionCardsData() data fetcher in packages/ui
- CollectionCardsClient 'use client' component in packages/ui
- Async RSC wrapper moved to packages/next
- Removed CollectionCards from @payloadcms/ui/rsc exports
… views

- Add getLoginViewData() in packages/ui for framework-agnostic login data
- Add getDocumentViewData() in packages/ui for comprehensive document data
- Add getEntityPreferences() utility in packages/ui (no React.cache dependency)
- Refactor Next.js LoginView and renderDocument to use shared data fetchers
- Document view data fetcher handles doc loading, permissions, versions,
  form state, slots, live preview — returns redirect info instead of calling redirect()
- Add getListViewData() in packages/ui for framework-agnostic list data
- Handles query processing, preferences, presets, document fetching,
  table rendering, filters, and slot rendering
- Accepts ComponentRenderer parameter for adapter-specific rendering
- Refactor Next.js renderListView to use shared data fetcher
- Add getRootViewData() in packages/ui for auth checks, first-user flow,
  collection/global resolution, client config, locale filtering
- Inlines isPublicRoute and handleAuthRedirect as pure helper functions
- Returns redirect URLs instead of calling framework-specific redirect()
- Adapter-specific route-to-component mapping stays in each adapter
- Add getAccountViewData() — document edit pipeline for current user
- Add getVersionViewData() — version comparison data with parallel fetches
- Add getVersionsViewData() — paginated version list with published/draft state
- All data fetchers are framework-agnostic, accept ComponentRenderer param
Implement dual-mode server functions (T1.2) — each RSC-returning handler
now has a data-only alternative that returns JSON-serializable data.

- Add ServerFunctionMode type ('rsc' | 'data-only') to DefaultServerFunctionArgs
- Create data-only handlers: renderDocument, renderList, renderWidget,
  renderField, renderDocumentSlots, getDefaultLayout
- Export dataOnlyServerFunctions registry from @payloadcms/ui
- Non-RSC adapters (TanStack Start) merge this registry with
  sharedServerFunctions instead of using the RSC handlers
…ions views

Refactor the remaining Next.js view components to delegate data fetching
to the framework-agnostic data fetchers in @payloadcms/ui:

- AccountView → getAccountViewData()
- VersionView → getVersionViewData()
- VersionsView → getVersionsViewData()
Audited codebase against the blocker list — mark resolved items
(CollectionCards, data-only server functions, view data fetchers,
rsc exports), detail remaining blockers (RootPage wiring,
React.cache, auth server actions, initReq, env var), and add
unblocking priority table.
… with prop

Move the Next.js-specific env var read out of RootProvider into the
Next.js adapter, passing it as an `enableRouterCacheRefresh` prop.
Non-Next adapters can supply their own value (or rely on the default
`false`).
Remove `import { cache } from 'react'` from five files to make
packages/ui fully framework-agnostic (React.cache is RSC-specific):

- getClientConfig, getSchemaMap, getClientSchemaMap: already have
  global caches (global._payload_*) making React.cache redundant
- getNavPrefs: replaced with a WeakMap keyed on the PayloadRequest
  object for equivalent per-request deduplication
- upsertPreferences (getPreferences): called once per operation,
  no deduplication needed
Refactor RootPage to delegate auth checks, first-user redirects,
client config generation, locale filtering, and visible-entity
resolution to the shared getRootViewData fetcher in packages/ui.

RootPage now only handles adapter-specific concerns: initReq,
getRouteData (Next.js component mapping), collection preferences
for folder views, not-found/redirect dispatch, and RSC rendering.

Also fix getRootViewData's getClientConfig user param to correctly
pass `true` for the createFirstUser case (when !dbHasUser), and
remove unused getEntityPreferences import.
All blockers requiring shared code changes are resolved:
- RootPage wired to getRootViewData
- React.cache() removed from packages/ui
- NEXT_PUBLIC_ENABLE_ROUTER_CACHE_REFRESH converted to prop
- Remaining items (initReq, auth server functions) are scoped
  to the new tanstack-start package
Phase T2 of the TanStack Start adapter implementation. Creates the
`@payloadcms/tanstack-start` package with:

- TanStackRouterAdapter: maps TanStack Router hooks to Payload's
  RouterAdapterContext (Link, params, pathname, searchParams, router)
- initReq: initializes Payload request from TanStack Start server
  context using @tanstack/react-start/server getRequest()
- ServerAdapter: framework-specific server utilities (cookies, headers,
  redirect, notFound) using TanStack Start/Router APIs
- handleServerFunctions: server function dispatcher using data-only
  mode (no RSC flight payloads), merging sharedServerFunctions with
  dataOnlyServerFunctions
- Auth server functions: login, logout, refresh, switchLanguage using
  createServerFn from @tanstack/react-start
- Supporting utilities: setPayloadAuthCookie, getExistingAuthToken,
  getPreferences, getRequestLocale
…ta loaders

Phase T3 of the TanStack Start adapter implementation. Adds the
view/layout layer that mirrors Next.js's RootLayout + RootPage pattern:

- RootLayout: HTML shell wrapping RootProvider with TanStackRouterAdapter,
  theme detection, RTL support, language options, and nav preferences
- getLayoutData: server-side loader fetching all data needed by the root
  admin layout (auth, i18n, client config, permissions, theme)
- getAdminPageData: catch-all page loader that uses getRootViewData for
  auth checks/redirects, then getRouteData for view resolution
- getRouteData: route-to-view-type resolver adapted from Next.js version,
  handling all admin routes (dashboard, collections, globals, auth views,
  folders, versions, custom views) without RSC component references
- Supporting utilities: getRequestTheme (framework-agnostic port)
- New package exports: ./layouts, ./views

The architecture provides three composable functions that users wire into
their TanStack Router routes: getLayoutData (layout loader) →
getAdminPageData (page loader) → client component rendering.
…p, and meta utilities

Phases T4-T7 of the TanStack Start adapter:

- T4: TanStackComponentRenderer wrapping RenderClientComponent — all
  custom Payload components must be client-safe (no async RSC)
- T5: viteDevReloadStrategy using import.meta.hot for Vite HMR
  full-reload detection
- T6: getImportMapOutputPath for adapter-specific import map location
- T7: getAdminMeta generating TanStack Router head.meta entries for
  admin page titles and viewport tags
… adapter

Rewrite auth server functions as plain async functions matching the
Next.js adapter pattern instead of createServerFn wrappers. Fix type
mismatches in router adapter, layout data, route resolution, and view
components. Add proper null guards, type casts, and Vite HMR type
declarations to pass build and lint with strict: true.
Wire up `pnpm dev:tanstack <suite>` to start a TanStack Start dev
server that loads any test config. Key pieces:

- test/tanstack-app: minimal TanStack Start app with file-based routes
  (__root, admin.$, admin.index, api.$, index) and a custom Vite SSR
  middleware that bypasses nitro's worker-based runner to avoid
  DataCloneError with native modules.
- test/adapters/tanstackStartDevServer.ts: spawns the local vite binary
  with the correct env vars and resolves once the server is ready.
- test/dev.ts, initDevAndTest.ts, testHooks.ts: framework-aware paths
  for import map generation, tsconfig patching and cache clearing.
- vite.config.ts: SCSS tilde-import resolver, SSR externals for native
  packages, and optimizeDeps exclusions matching withPayload.js.
@paulpopus paulpopus mentioned this pull request Apr 7, 2026
r1tsuu added 4 commits April 7, 2026 19:08
…stack-app to root

Move test/tanstack-app/ to root tanstack-app/ as its own workspace package.
Split @payloadcms/tanstack-start/views into client-only (./views) and
server-only (./views/server) entry points. Route files now use
createServerFn wrappers with dynamic imports, eliminating ~200 lines of
Node.js module stubs from vite.config.ts.
…ed rendering

Extract buildListViewClientProps and buildDocumentViewClientProps from
@payloadcms/ui to reconstruct React nodes from serializable data on the
client using RenderClientComponent + importMap. Rewrite AdminView.tsx to
delegate to DefaultListView, DefaultEditView, DashboardView, and
LoginForm. Import admin SCSS in Root layout for proper styling. Pass
fieldPermissions through to renderTable so columns render correctly.
…dev server

Stub server-only modules (pino, sharp, busboy, file-type, etc.) to prevent
them from leaking into the client bundle and breaking React hydration.
Add Payload base styles import to the root component. Forward request body
in the SSR middleware for POST/PATCH/PUT methods using Readable.toWeb().
@r1tsuu r1tsuu force-pushed the experiment/framework-adapter-pattern branch from b0ca0a2 to 0a20737 Compare April 7, 2026 23:23
- Import CollectionCards SCSS in client component to ensure CSS loads
  when the widget is resolved dynamically via the import map
- Replace simplified DashboardNavGroups with proper CollectionCardsClient
  in TanStack Start, matching the Next.js dashboard layout
- Silence Sass deprecation warnings for import and global-builtin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants