Skip to main content
The panel ships a command palette (Cmd+K / Ctrl+K, or the sidebar search button) that searches records across the panel and offers quick actions alongside the results. An add-on contributes to both without touching any panel source: a SearchSource makes its own entities findable, and a SearchCommand adds a static verb to the palette.

Search sources

A search source describes one entity the palette can find records in: Lunar\Panel\Search\SearchSource.
  • key() — the source’s stable identifier. It doubles as the kinds[] filter value and the result group’s key.
  • label() — the group heading shown above this source’s results (e.g. “Products”).
  • query() — the base query: model, eager loads, scopes. Both matching paths (see below) start from here, so a constraint added here (a soft-delete scope, a store filter) always applies.
  • applyTerm(Builder $query, string $token) — narrows the query by one search token. The resolver splits the search term on whitespace and calls this once per token, ANDing them together, so word order in the query does not matter.
  • row(Model $model) — shapes a matched record into a result row. hint is the disambiguating detail shown under the label (an order’s customer name, a product’s SKU); return null when there is none.
  • permission() — a manifest permission handle. Declare the same handle that gates this entity’s own routes and navigation, so search can never surface a record the user could not otherwise reach. null makes the source visible to every panel user.
  • icon() — a name from the panel’s built-in icon set, shown next to each result row.
  • position() — a Position, ordering this source’s result group relative to others.

Worked example

src/Search/CustomerEmailSearchSource.php in panel-addon-example searches customers by account reference:

Registering a source

Return SearchSource classes from Section::searchSources():

Search commands

A search command is a static, always-available verb the palette lists alongside record results — typically a create shortcut: Lunar\Panel\Search\SearchCommand.
Commands are resolved once per request, filtered by permission, and shared to the frontend as an Inertia prop — the palette filters them against the translated label client-side, with no round trip.

Worked example

src/Search/PingWidgetsCommand.php:

Registering a command

Return SearchCommand classes from Section::searchCommands():

How matching works

The palette’s endpoint (GET /{panel path}/search, route name panel.search) fans a query out across every source the current staff member is allowed to see, each contributing at most five rows so no single source crowds out the rest. Request params: q (the search term) and an optional kinds[] array narrowing the fan-out to selected source keys (the palette’s filter chips). The response shape is uniform across sources:
Matching happens one of two ways, chosen per source automatically:
  • SQL LIKE (the default) — the term is tokenised and every token must match one of applyTerm()’s clauses. This is what a source gets with no further configuration, and it forgives word order and partial words, but not misspellings.
  • Scout, opt-in — enable lunar.panel.search.scout_enabled (config key search.scout_enabled in config/lunar/panel.php, default false, also settable via the LUNAR_PANEL_SEARCH_SCOUT environment variable) to route matching through Laravel Scout for any source whose model uses the core Searchable concern. Typo tolerance then comes from whichever search engine backs Scout (Meilisearch and Typesense both tolerate typos out of the box; Scout’s database driver does not, so it behaves like the LIKE path). A source whose model is not Scout-indexed falls back to LIKE automatically, so a store can mix indexed and unindexed sources.
A source only ever declares its base query and its applyTerm() clauses — it gets the Scout path for free when its model is indexed, with no extra code.
Sources and commands both declare a permission() handle. Gate it identically to the routes and navigation for the same entity — the search endpoint enforces this per source, so a staff member without the permission never sees the source’s results, even if they know the exact term.