Skip to content

feat: admin framework adapter pattern and tanstack support#16139

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

feat: admin framework adapter pattern and tanstack support#16139
r1tsuu wants to merge 60 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

r1tsuu and others added 30 commits March 31, 2026 20:33
- Add BaseAdminAdapter and AdminAdapterResult types to packages/payload/src/admin/adapter/types.ts
- Add createAdminAdapter helper factory (mirrors createDatabaseAdapter pattern)
- Add admin.adapter field to Config type
- Wire adapter.init() into Payload lifecycle after db init
- Export types and helper from packages/payload/src/index.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add RouterProvider with value-based context (params, pathname, router, searchParams, Link)
  — avoids dynamic hook calls that react-compiler rejects
- Add useRouter, usePathname, useSearchParams, useParams hook wrappers reading from context
- Integrate RouterProvider into RootProvider via optional router prop
- Export RouterProvider, useRouter, usePathname from public client index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2: Replace all next/navigation imports in packages/ui (~41 files)
with RouterProvider hooks. Add NavigateOptions to RouterInstance for
scroll support. Replace AppRouterInstance, ReadonlyURLSearchParams, and
next/link LinkProps type imports with framework-agnostic equivalents.

Phase 3: Remove all Next.js dependencies from packages/payload.
- Replace @next/env with dotenv + dotenv-expand in loadEnv.ts
- Replace Metadata type from 'next' with PayloadMetadata subset
- Remove ReadonlyRequestCookies from getRequestLanguage (use Map)

Phase 5: Create nextAdapter() factory in packages/next implementing
the BaseAdminAdapter interface. Add NextRouterProvider wrapping
next/navigation hooks into the RouterProvider context. Wire
NextRouterProvider into the Root Layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 4 (partial): Split Login and NotFound views into server entry
(packages/next) + render component (packages/ui). Login server entry
handles redirect when user is logged in, delegates rendering to
framework-agnostic LoginView in packages/ui. NotFound client component
moved to packages/ui.

Refactor all 11 client components in packages/next that imported from
next/navigation to use RouterProvider hooks from @payloadcms/ui instead.

Replace deprecated useSearchParams/useParams exports from SearchParams
and Params providers with the Router provider versions. Add views/*
export path to @payloadcms/ui package.json.

Add ServerNavigation context (provider + hook) for future use by client
components that need notFound/redirect access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ges/ui

Move Login, NotFound, ForgotPassword, Logout, Verify, CreateFirstUser,
Unauthorized, and ResetPassword views to packages/ui. These views have
zero Next.js dependencies and are now fully framework-agnostic.

packages/next view files become thin re-exports for backwards
compatibility. Login retains its server entry for redirect handling.

Also move FormHeader and Logo elements, plus getDocPreferences and
getDocumentData utilities to packages/ui.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the entire Dashboard view (18 files, ~2400 lines) from
packages/next to packages/ui. Convert all @payloadcms/ui self-imports
to relative paths. Move getPreferences utility. Add @dnd-kit/modifiers
dependency to packages/ui.

packages/next Dashboard entry becomes a thin re-export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n prompt

Add --framework / -f flag with 'next' (default) and 'tanstack-start' options.
Add interactive selectFramework() prompt following the same pattern as
selectDb(). Add FrameworkType to types and frameworkType to templates
and createProject args.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Proof-of-concept AdminAdapter implementation for TanStack Start.
Implements the full BaseAdminAdapter interface with stub methods that
show the intended @tanstack/react-router and vinxi/http integration.

Replace stub hooks in RouterProvider.tsx with real @tanstack/react-router
imports to activate. Replace stub methods in adapter/index.ts with
vinxi/http cookie calls and TanStack navigation throws.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Test suite covers:
- adminAdapter is undefined without explicit configuration (backwards compat)
- admin.adapter config field accepts AdminAdapterResult
- adapter.init() is called and returns adapter instance
- createAdminAdapter helper is an identity function

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a framework-agnostic FrameworkTestHarness interface so e2e
tests can run against any admin adapter, not just Next.js.

- FrameworkTestHarness interface with start()/stop() contract
- NextJsTestHarness wrapping existing dev-server behavior
- getTestHarness() factory reading PAYLOAD_FRAMEWORK env var
- Stub for TanStack Start harness (pending implementation)

Select framework via: PAYLOAD_FRAMEWORK=next|tanstack-start

CI matrix usage:
  strategy:
    matrix:
      framework: [next]  # add tanstack-start once harness is implemented
  env:
    PAYLOAD_FRAMEWORK: ${{ matrix.framework }}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
26-task plan covering:
- Moving all framework-agnostic code from packages/next to packages/ui
  (elements, templates, utilities, view render functions with nav callbacks)
- Creating a shared handleServerFunctions dispatcher in packages/ui
- Full tanstack-start adapter with vinxi/http, @tanstack/react-router
- tanstack-app/ shell (app.config.ts, routes, layouts, entry points)
- pnpm dev:tanstack command and test/dev-tanstack.ts entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ith navigation callbacks

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ges/ui

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… shared server function dispatcher

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…and @tanstack/react-router

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add framework-agnostic view utilities: getRouteData, getCustomViewByRoute,
  getCustomViewByKey, getDocumentViewInfo, isPathMatchingRoute, attachViewActions
- Create RenderRoot.tsx accepting initPageResult + notFound/redirect callbacks
- Create DocumentView, ListView, BrowseByFolder, CollectionFolders, CollectionTrash
  framework-agnostic wrappers that don't import from next/navigation
- Add views/Root/RenderRoot export to packages/ui package.json
- Update packages/next RootPage to delegate to renderRootPage from packages/ui

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Create tanstack-app workspace with package.json, app.config.ts, tsconfig
- Add tanstack-app entry points: client.tsx, ssr.tsx, router.tsx, routeTree.gen.ts
- Create root layout route (__root.tsx) with server function dispatcher
- Create admin (admin.$.tsx) and API (api.$.ts) routes
- Add packages/tanstack-start RootLayout server component using vinxi/http
- Add packages/tanstack-start RootPage delegating to packages/ui renderRootPage
- Add packages/tanstack-start routes re-exporting REST handlers from packages/next
- Add dev:tanstack and dev:tanstack:postgres npm scripts
- Update initDevAndTest to support TanStack app importMap path (app/importMap.js)
- Add tanstack-app to pnpm-workspace.yaml

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Switch tanstack-app from Vinxi/app.config.ts to Vite/vite.config.ts
- Use @tanstack/react-start (not @tanstack/start) with @vitejs/plugin-react
- Update initReq: getWebRequest() → getRequest() from @tanstack/react-start/server
- Update adapter/RootLayout: vinxi/http cookies → @tanstack/react-start/server
- Update createServerFn API: validator → inputValidator, new ssr.tsx signature
- Add getRouter() function (replaces createRouter()) as required by plugin
- Add scss tilde import alias, sharp optimizeDeps.exclude, sass devDependency
- Add @payloadcms/next and @payloadcms/richtext-lexical to tanstack-app deps
- Fix initDevAndTest.ts importMap path for TanStack apps
- Smoke test: pnpm dev:tanstack serves HTTP 200 at /admin

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… as devDep

Replaces @tanstack/start and vinxi peer deps with @tanstack/react-start.
Adds @tanstack/react-start as devDependency so tests resolve the import.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add tanstackStartCompatPlugin() to stub tanstack-start-injected-head-scripts:v
  virtual module missing from @tanstack/start-server-core@1.167.9
- Add admin.index.tsx route for /admin base path (splat doesn't match empty)
- Fix RenderRoot.tsx: empty segments must pass null path to formatAdminURL
  to match adminRoute without trailing slash

Dashboard, collections list, and create document all working.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Port 3000 for dev:tanstack (same as Next.js, enables running e2e suites)
- Auth redirect moved to route loader (loader redirects work in TanStack Router)
- admin.index.tsx loader handles unauthenticated access redirect
- admin.$.tsx loader handles /collections, /globals → /admin redirect
- server-only-stub.js: ESM stub for server-only packages (file-type, pino, mongoose, util)
- serverOnlyStubPlugin in vite.config.ts intercepts client bundle for server packages
- tanstackStartCompatPlugin stubs tanstack-start-injected-head-scripts:v virtual module
- Fix StartClient import: @tanstack/react-start/client (not @tanstack/react-start)
- Fix RenderRoot.tsx: empty segments → null path (prevents /admin/ trailing slash)
- admin.index.tsx route added for /admin base path

Known issue: TanStack Start is isomorphic (all code runs client+server).
Server-only Payload code (DB adapters, sharp, etc.) leaks into client bundle.
Full RSC isolation requires createServerFn wrappers — tracked for future work.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
r1tsuu and others added 2 commits April 2, 2026 08:23
…ldTableData split

- Phase 0: extract buildTableData from buildTableState in packages/ui (RSC-free list data)
- Task 1: getPageState calls buildTableData for list views, returns listData in SerializablePageState
- Task 3: TanStackListView uses ListQueryProvider + SelectionProvider + DefaultListView directly (no server fn call on mount, no Table:ReactNode)
- Task 4: TanStackAdminPage passes listData to TanStackListView
- Constraint note: packages/ui change is one new file + one export line only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Codex <codex@openai.com>
r1tsuu and others added 27 commits April 2, 2026 10:08
Rewrite the TanStack app @payload-config alias before Vite starts, support suite#config overrides in dev:tanstack, and make e2e runs launch dev:tanstack when PAYLOAD_FRAMEWORK=tanstack-start.
Replace the server-only TanStack admin route flow with serializable page state, client-side admin rendering, and supporting TanStack root/server-function wiring. Remove the old server-only stubs and update related exports, app routes, and test types.
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
Decouple root, dashboard, and default template rendering from direct server-component resolution so framework adapters can provide their own renderer. This keeps Next on the current RSC path while establishing the shared UI boundary needed for TanStack support.
Continue moving admin composition behind the injected renderer so shared UI no longer owns direct component execution in nav, dashboard, list, and document paths. This keeps the Next runtime behavior intact while reducing the work TanStack needs to replace duplicated admin rendering.
Clarify that the next TanStack step is to collapse `TanStackAdminPage` onto shared template and view contracts instead of extending its custom wrappers.
Route the TanStack admin dashboard, nav, list, and document paths through shared ui entrypoints so adapter-specific code stays thin and framework behavior converges around the same contracts.
Clarify that the immediate migration target is the shared root/bootstrap path needed to load the first admin page, with nav only serving as one example of the boundary problem.
Move root bootstrap resolution and default shell composition into shared UI contracts so Next and TanStack consume the same first-page path.
Clarify that the root-first adapter work must extend across all built-in admin views and that TanStack should adopt a Next-like per-view folder structure instead of growing a monolithic admin page entrypoint.
Explain that reusable admin-side server logic can remain in `@payloadcms/ui`, while framework-specific invocation transport belongs in `@payloadcms/next` and `@payloadcms/tanstack-start` because not every runtime will support server actions.
Move the simplest TanStack auth and minimal view branches into per-view modules and document that shared ui auth view entries should be the long-term implementations reused across Next and TanStack.
Clarify that the same adapter-to-shared-ui structure should apply to dashboard, list, document, edit, account, and related built-in views, not just the auth and minimal view slice.
Clarify that TanStack's current dashboard extraction is only transitional and that non-RSC-specific modular dashboard logic should move from Next into shared ui for reuse across frameworks.
Eliminate the intermediate re-export file for ModularDashboard, allowing direct imports from @payloadcms/ui. Update related documentation to emphasize the importance of avoiding unnecessary indirection in adapter packages. Clean up tests and imports to reflect these changes.
…and imports

Eliminate unnecessary components and imports from TanStackAdminPage, simplifying the structure and improving maintainability. This includes the removal of ListView, AccountView, and UnsupportedView, along with related logic, to focus on essential functionality.
Replace ~@payloadcms/ui/scss with relative paths in 28 SCSS files
moved from packages/next to packages/ui — the webpack ~ prefix cannot
self-resolve within the same package via esbuild's sass-plugin.

Fix missing InitPageResult properties (languageOptions, translations,
importMap, i18n, payload, viewType) in BrowseByFolder and
CollectionFolders server function handlers.

Fix TanStackViewType → ViewTypes cast in buildRenderViewArgs.ts.
Add TanStack BrowseByFolder and CollectionFolders view adapters that
fetch data via server functions and render shared UI components.

Register render-browse-folder and render-collection-folder handlers in
the shared server function registry.

Update getViewByType dispatcher with folder and version view types.

Add architecture overview doc for the framework-agnostic admin pattern.
Update the Dashboard view implementation to introduce a shared async builder (`renderDashboardView`) for data fetching and rendering. Modify the Next.js adapter to utilize this builder, improving the separation of concerns between the shared UI and framework-specific logic. Additionally, update exports to include the new builder and related types, streamlining the integration across frameworks.
Extract fetchListViewData from renderListView into a shared, serializable
data function. Move list, document, and account data fetching into the
TanStack getPageState loader so views render immediately from loader data
instead of showing a LoadingOverlay while useEffect fetches via server
function.

- List: shell (header, controls, pagination) renders instantly; Table
  loads async via server function
- Document: renders DocumentInfoProvider + DefaultEditView directly from
  loader data; falls back to server function for version subviews
- Account: same pattern as Document with useEffect fallback
Document the two-layer split (data function + builder), the hybrid
approach for non-serializable parts, and updated file structure
reflecting fetchListViewData and getPageState loader integration.
…oot layout

- Fix boolean prefetch attribute warning on TanStack Link by mapping to preload prop
- Export RootLayoutData type from client entrypoint
- Export getRootLayoutData from server entrypoint
- Switch __root.tsx to loader-based data fetching with getRootLayoutData
- Add switchLanguage server function for i18n cookie management
Plan for refactoring render-widget, render-list, render-document server
functions to return serializable data by default while preserving RSC
rendering behavior in Next.js via extraServerFunctions override.
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.

1 participant