Global search that only shows what you may open: one service, per-source permissions, an extension hook
The header search was a demo box with no data behind it. On 12 October it became a real search over users, roles, permissions and Knowledge Base documents, where each source is gated by its own permission and a package can add a source in one call.
Until 12 October 2026 the search box in the Flightpack header was a demo. It had a nice dropdown and no data behind it. Ticket FP-180 replaced it with a real search, and the interesting part was not the matching. It was deciding who may see which result.
What it searches
Four sources: users, roles, permissions and Knowledge Base documents. Typing in the header shows a short list of suggestions, five at most. Pressing enter opens a results page, which can show everything or one source at a time, with paging of up to 50 results per page. A term must be at least two characters and at most one hundred, and the server enforces both, not only the browser.
One service, and permission per source
A single SearchService owns the whole thing. Each source is registered with the permission needed to search it: users need users.view, roles need roles.view, permissions need permissions.view. Before searching, the service asks which sources the signed-in user may use, and searches only those. A person who cannot open the user list cannot find a user through the search box either. Search is a second door into the same rooms, so it uses the same locks.
source asked of the caller users users.view roles roles.view permissions permissions.view documents the Knowledge Base levels the caller may read
This is the rule I would copy first. Many search boxes are built as one big query and filtered afterwards, and the filter is where the leak is. Deciding the sources first means there is nothing to filter out.
Repositories, not queries in the service
The service does not write SQL. Each repository gained one small search method, and the service maps the paged result to a common shape: a title, a type and the path to open. The same layering as everywhere else, described in the architecture on one page, so the matching rules for users stay in the user repository where the next developer will look for them.
A package can add a source in one call
The service has a registration method that takes a name, a search closure and the permission it needs. A package that owns data, for example a tracker of tickets, adds its own source from its service provider and appears in the dropdown and on the results page with no change to the core. The permission is part of the registration, so a package cannot forget it. This is the same idea as the rest of the package contract: the core offers a place to plug in, and the plug carries its own rules.
The route and its counter
All search routes sit behind sign-in and a limit of 60 requests a minute. The limit has its own counter named search, which matters because a search box is the one control a person hits many times in a row, and a shared counter would lock them out of unrelated screens. The story of that mistake is in the post on throttle counters. The new routes were also added to the route allowlist that the security checks read, so an endpoint nobody listed cannot appear.
What we left out on purpose
The ticket also mentioned bookmarks, navigation badges and a breadcrumb store. We did not port them. They belong to station and device features of the product the pattern came from, and the core has neither. The ticket says so in its notes, which is the part of the record I trust most: what was left out, and why.
- Tested. A test file with nine cases covers the search, and the acceptance line that matters says results respect permissions.
- Status. The ticket is marked Done in code, not Done: committed and tested, and still waiting for a human run in a real environment.
- Not claimed. This is a search over four sources by text match. It is not ranking, typo tolerance or full-text indexing, and I would not describe it as any of those.
What I would tell another small team
Decide which sources a person may search before you search them, and make every new source register its permission when it registers itself. A search box that cannot show what you may not open is worth more than one that ranks well.