Skip to content

feat(admin-search)!: add baseFilter to scope search results per request - #201

Draft
jhb-dev wants to merge 5 commits into
mainfrom
feat/admin-search-base-filter
Draft

feat(admin-search)!: add baseFilter to scope search results per request#201
jhb-dev wants to merge 5 commits into
mainfrom
feat/admin-search-base-filter

Conversation

@jhb-dev

@jhb-dev jhb-dev commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What

Adds a baseFilter option (({ req }) => Where | Promise<Where>) that restricts search results to a constraint resolved against the current request. The filter runs on the server and is combined with the typed query using and, so results stay in scope even before anything is typed.

The main use is multi-tenancy: scoping the search to the tenant selected in the admin panel.

adminSearchPlugin({
  baseFilter: ({ req }) => {
    const tenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
    return tenant ? { tenant: { equals: tenant } } : {}
  },
})

This scopes what the search offers, not what the API permits — it is not access control. The README points at searchOverrides.access.read for constraining GET /api/search server-side.

BREAKING

The header component is now a server component so it can evaluate baseFilter before rendering. It moved:

  • @jhb.software/payload-admin-search/client#SearchWrapper@jhb.software/payload-admin-search/rsc#SearchWrapper
  • the /client export now provides SearchWrapperClient in place of SearchWrapper

Consumers must run payload generate:importmap after upgrading, otherwise the search component resolves to nothing and disappears from the admin header.

Per the repo's breaking-change policy this is a minor bump (plugin is pre-1.0).

Tests

  • src/components/SearchWrapper/resolveBaseFilter.test.ts
  • src/components/SearchModal/buildSearchQuery.test.ts — the base filter and the typed query are combined with and

Vitest is newly configured for this plugin (vitest.config.ts).

Dev app demonstration

dev/ gains a tenants collection, multi-tenant plugin wiring in dev/src/payload.config.ts, a baseFilter example in the plugin invocation, and seeded per-tenant documents. Switching tenants in the admin panel changes what the search returns.

jhb-dev and others added 4 commits August 23, 2026 22:10
The documented baseFilter recipe restricted results to `{ tenant: { equals:
tenant } }`, which also drops every indexed document that carries no tenant.
Search indexes are usually wider than the set of tenant-scoped collections, so
picking a tenant made shared collections disappear — in the dev app, every
author and media item. The README and the dev app now admit un-tenanted
documents via `or` + `exists: false`.

Also harden the surrounding code:

- A baseFilter that throws no longer fails the admin panel's render. It runs
  during server render, so an exception replaced the whole page rather than
  just the search. It narrows a query rather than granting access, so the
  fallback is an unscoped search plus a logged error.
- Cover the config wiring the feature depends on: preserving an incoming
  `custom` config, storing the options for the server component to read back,
  and appending rather than replacing existing admin actions.
- Document that the resolved filter is readable in the browser, and that
  mounting SearchWrapperClient by hand yields an unscoped search.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lth5KTeFjEugJcWXYUoKya
…ad already built

The search component built its own request from `next/headers` via
`createLocalReq`, but Payload builds one for this very render and passes it to
admin components as a server prop. Its own copy therefore duplicated work and
came out subtly different: `@payloadcms/next`'s `initReq` resolves the locale
onto the request it builds, so the rebuilt one fell back to the default locale,
and its i18n was the admin panel's client-facing subset cast to a server one.

Take the request Payload passes instead. `next/headers`, `createLocalReq` and
the i18n cast all go away, and a filter reading `req.locale` now sees the locale
the panel is being viewed in.

`req` is part of what Payload passes at runtime but not of its exported
`ServerProps` type, so it is declared on the component's own props. If a future
version stops passing it, a configured filter cannot run and the search would
silently widen to every document — that case is logged rather than ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lth5KTeFjEugJcWXYUoKya
@jhb-dev
jhb-dev force-pushed the feat/admin-search-base-filter branch from 75ec994 to e006105 Compare August 23, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants