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 thekinds[]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.hintis the disambiguating detail shown under the label (an order’s customer name, a product’s SKU); returnnullwhen 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.nullmakes 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()— aPosition, 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
ReturnSearchSource 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.
Worked example
src/Search/PingWidgetsCommand.php:
Registering a command
ReturnSearchCommand 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:
- SQL
LIKE(the default) — the term is tokenised and every token must match one ofapplyTerm()’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 keysearch.scout_enabledinconfig/lunar/panel.php, defaultfalse, also settable via theLUNAR_PANEL_SEARCH_SCOUTenvironment variable) to route matching through Laravel Scout for any source whose model uses the coreSearchableconcern. Typo tolerance then comes from whichever search engine backs Scout (Meilisearch and Typesense both tolerate typos out of the box; Scout’sdatabasedriver does not, so it behaves like theLIKEpath). A source whose model is not Scout-indexed falls back toLIKEautomatically, so a store can mix indexed and unindexed sources.
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.