Every security analyst has a daily ritual that nobody put in a runbook. Open the findings list, set severity to high and critical, scope to the company you are working on this week, hide the columns you never read, sort by SLA deadline, and only then start triaging. The work that matters begins after a dozen clicks that produce nothing of value. Repeat that on the asset list. Repeat it on the scan list. Multiply by every analyst on the team and every shift in the year, and a surprising amount of expert time disappears into reconstructing the same view from scratch.
Saved filters and views exist to delete that ritual. They turn a filter configuration you build once into a named preset you can recall in a single action, scoped to the exact list page where it belongs. In PMAP this is a deliberate, thin layer that sits alongside every major list surface and persists exactly what a user has set up, nothing more and nothing less. This article walks through how that layer works, what the difference between a saved filter and a saved view actually is, which ten entity types support presets, and why the design choices behind per-user scoping and opaque criteria storage matter more than they first appear.
If you want the broader context for where this preference layer fits in the product, the vulnerability management platform architecture pillar maps the surrounding domains. This piece zooms into one of them.
The Cost of Re-Typing the Same Filter Every Day
The hidden tax of list-page work is not the triage itself. It is the setup that has to happen before triage can start. A finding list with thousands of rows is useless until it is filtered to the slice a person is responsible for right now. The filtering knowledge lives in the analyst’s head, and every session begins by transcribing that knowledge into the UI by hand.
This has three real costs. The first is wasted time, the most visible and least important of the three. The second is inconsistency. When ten people each reconstruct the same conceptual view by memory, they make small differences without noticing. One person filters severity to high and critical. Another includes medium because they always have. Their numbers diverge, and nobody can say why. The third cost is error. A misremembered filter quietly hides rows that should have been seen. In vulnerability management a hidden row is a missed obligation, and a missed obligation is how a known issue ages past its SLA without anyone choosing to let it.
Saved filters address all three at once. The saved filter domain in PMAP exists specifically to let authenticated users persist and reuse filter and view configurations for any entity list. Instead of rebuilding a view, you select it. The configuration is captured once by the person who understands it, then recalled identically every time. Recognition replaces recall, which is the older and more reliable of the two cognitive modes. The Nielsen Norman Group has written extensively on why interfaces that let people recognize an option beat interfaces that force them to remember and retype one. Saved presets are that principle applied to security list pages.
Saved Filter vs Saved View: Two Kinds
The first thing to understand is that a saved preset is not one thing. PMAP records a kind field on every preset, and it takes one of two values: query or view. The distinction looks small and is actually the core of the feature.
A query preset stores filter and search state only. It captures the answer to the question “which rows do I want to see.” Severity thresholds, company scope, status filters, date ranges, free-text search terms, all of that is query state. When you apply a query preset, the underlying list refilters to the rows you described, but it leaves the rest of the page exactly as it was.
A view preset stores everything a query preset stores, plus presentation state. This is the answer to a second, independent question: “how do I want to look at those rows.” Which columns are visible and which are hidden, the sort order, the layout. A view preset bundles the what and the how into a single named object so that applying it reconstructs the entire working surface, not just the row set.
The defaulting behavior here is worth knowing because it shapes how presets behave in practice. If kind is omitted or invalid when a preset is created, PMAP stores it as view. The richer kind is the assumed intent. And once a preset is created, its kind is immutable. The update path does not accept a new kind value, so a query preset stays a query preset for its whole life and a view preset stays a view. If you decide later that you wanted the other kind, you create a new preset rather than mutating the existing one. This keeps the meaning of any given preset stable and predictable, which matters when other people on a list page rely on presets behaving consistently.
When a View Also Stores Columns, Sort and Layout
The presentation state inside a view preset is the part analysts feel most. Consider a triage view of the findings list. The query half narrows to open findings, high and critical severity, assigned to me. The view half then hides the columns that add noise during triage, surfaces the SLA deadline column, and sorts ascending by that deadline so the most urgent work floats to the top. The result is a single named view that, when applied, drops you directly into a ready-to-work surface.
Compare that to a reporting view of the same findings list. The query half might widen to all statuses across a full quarter. The view half then shows the columns a report consumer cares about, hides the operational fields, and sorts by company. Same list page, same underlying data, two completely different working surfaces, each captured once and recalled on demand. The query and view kinds let you choose how much of that surface you want a preset to own.
Ten Entity Types You Can Save Presets For
Saved presets are not limited to findings. Every preset targets a specific list page through its entity_type field, and the domain validates that field against a fixed enum of ten values. The ten entity types are: asset, asset_group, finding, project, scan, company, report, dashboard, team, and ticket.
That coverage is broader than most people expect, and the breadth is the point. The same preference layer powers every major list surface in the product rather than being a one-off bolted onto the findings page. An analyst who lives in findings, an asset owner who lives in the asset and asset-group lists, a program manager who works from projects and companies, and an operations lead who tracks scans and tickets all get the same saved-preset behavior on their respective pages.
Two rules govern how entity scoping works. First, an invalid entity_type on create returns a 400 Bad Request. You cannot accidentally create a preset for a page that does not exist, which keeps the data clean and the frontend predictable. Second, entity_type is immutable after creation, exactly like kind. A finding preset cannot be retargeted to assets. This is a sensible constraint because a preset’s criteria are written in the vocabulary of one entity’s list page. Finding filters reference finding fields. Asset filters reference asset fields. Letting a preset jump entities would leave its criteria meaningless, so the design forbids the move and asks you to create a fresh preset for the other entity instead.
The practical effect is that each list page shows only the presets that belong to it. The saved filters panel on the findings page queries for entity_type=finding and sees finding presets. The asset page sees asset presets. Presets never bleed across pages, and the enum guarantees there is a known, finite set of surfaces this layer serves.
Default Presets and Default-First Ordering
A preset you have to go find is better than no preset. A preset that applies itself the moment you open a page is better still. PMAP supports this through a single boolean field, is_default, on every preset.
When a preset is flagged is_default = true, the frontend can auto-apply it as soon as the user opens the relevant list page. The most common arrangement of a list is no longer the empty default state. It is the view the user already decided is their starting point. An analyst opens findings and lands directly in their high-and-critical, assigned-to-me triage view without touching a control.
The ordering rule that supports this is deliberately simple. The list endpoint returns presets ordered is_default DESC, updated_at DESC. Defaults come first, and within that, the most recently updated preset leads. This default-first ordering means the preset most likely to matter is always at the top of any list the frontend renders, whether that is a dropdown of presets or an auto-apply lookup.
One detail here is honest rather than idealized, and it is worth stating plainly because it changes how you should think about defaults. The is_default column has no uniqueness constraint. The database allows more than one default for the same user and entity type. PMAP does not silently enforce a single canonical default at the storage layer. Instead, the default-first ordering does the work: if more than one preset is flagged default, the most recently updated one surfaces first. So the system always has a well-defined answer for which default to apply, even though it does not forbid you from marking several. Understanding this prevents the surprise of marking a second default and finding the first still exists. The newest default wins by ordering, not by exclusion.
Strict Per-User Scoping, No Shared Visibility
This is the design decision that most shapes how saved presets behave, and it is unambiguous. Saved filters are scoped strictly to the owning user. There is no shared visibility and no team-level preset. Every preset is owned by and visible only to the user who created it.
The mechanism is the user_id field, populated from the JWT claims of the authenticated request. The list endpoint filters by that user_id, so a user’s request returns only that user’s presets. Nobody sees anyone else’s. There is no toggle to expose a preset to a colleague, no concept of a public or team preset in this domain.
That is a feature, not a limitation, and the reasoning holds up. A saved preset is a personal working tool. The way one analyst likes to slice and sort the findings list is theirs, tuned to how they think and what they own. Sharing presets across users would invite collisions, accidental edits to a teammate’s carefully built view, and arguments about whose default wins on a shared page. By keeping presets strictly personal, PMAP makes them safe to experiment with. You can create, rename, and delete your own presets freely, knowing you cannot disturb anyone else’s setup and nobody can disturb yours. This per-user scoping is also a clean application of least-privilege thinking. OWASP guidance on per-user access scoping treats data that belongs to a single user as data only that user should read or mutate, and the saved filter domain enforces exactly that.
Sparse Updates and Owner-Enforced Mutations
How presets are edited matters as much as how they are stored, and two behaviors define editing in this domain.
The first is the sparse update. The PUT /{id} endpoint uses a partial patch pattern. Only the fields you actually supply get written. The updatable fields are name, criteria, view_state, and is_default, and any field you omit retains its current value. If you only want to rename a preset, you send the new name and nothing else, and the criteria, view state, and default flag are untouched. If you only want to promote a preset to default, you send is_default alone. This matters because it makes partial edits safe. You never have to send a full preset object back just to change one attribute, and you can never accidentally blank out criteria by leaving them out of an update. There is a small companion rule that protects the same thing: an empty criteria value on update is ignored rather than written, so a malformed or empty patch cannot wipe a preset’s filter state.
The second behavior is owner-enforced mutation, and it is the security backbone of the edit path. Both update and delete operations carry WHERE id = $1 AND user_id = $2 in their SQL. A preset can only be modified by the user who owns it. If the IDs match but the user_id does not, no row is affected and the operation returns a not-found error. This is the right shape for an authorization failure on a personal resource. It does not leak the existence of another user’s preset by returning a different error for “exists but not yours” versus “does not exist.” Both look identical from the outside. The combination of list-time user_id filtering and mutation-time user_id matching means there is no path, read or write, by which one user touches another user’s presets.
An Opaque Criteria Payload That Survives Schema Change
One of the quieter but more durable design choices in this domain is how it stores filter content. The criteria and view_state fields are opaque json.RawMessage blobs. PMAP stores and returns them verbatim. The saved filter domain does not parse, validate, or understand the internal structure of what a finding filter or an asset view actually contains.
This sounds like the domain is being lazy. It is the opposite. By treating filter content as an opaque payload, the saved filter layer decouples itself completely from the schema of every entity it serves. The findings list can add a new filter dimension next quarter. The asset list can change how it represents a column layout. Neither change requires a single line of work in the saved filter domain, because that domain never claimed to know what was inside the blob in the first place. The frontend writes whatever filter shape its list page uses, and the saved filter layer faithfully hands it back the next time it is asked.
This is the JSON-as-opaque-payload pattern doing exactly what it is good at. JSON’s data model, formalized in RFC 8259, is well suited to carrying structured content whose shape is owned by the producer and consumer rather than the transport in the middle. The saved filter domain is that transport and storage in the middle. It guarantees fidelity, not interpretation. The result is a layer that can serve ten different entity types, each with its own filter vocabulary, without ever needing to track ten evolving schemas. There is one more small rule that keeps the data tidy: if criteria is empty on create, it is stored as {} rather than as null, so the payload is always valid JSON when it comes back.
How Multi-Tenancy Holds Without a company_id Column
A reader who knows the rest of PMAP will notice something missing from the saved filter data model. Most domains in a multi-tenant platform carry a company_id to enforce tenant isolation. The saved filter table has no such column. That is not an oversight, and understanding why is a small lesson in how the platform’s auth model composes.
Tenant isolation is still complete here. It is achieved implicitly through user_id. A user_id is globally unique and is bound to a single tenant by the auth layer above this domain. A user cannot exist in two tenants, so scoping every preset to its owning user_id automatically scopes it to that user’s tenant as well. There is no way for a preset to be visible across tenant boundaries, because the only access path is the user’s own user_id, and that user_id belongs to exactly one tenant.
This is a clean example of not repeating an invariant that a higher layer already guarantees. The auth layer establishes that a user belongs to one tenant. The saved filter domain leans on that fact rather than duplicating it with a redundant company_id. The result is a simpler table and one fewer place where a tenant-leak bug could hide. Isolation is inherited from identity, not bolted on as a separate column.
How PMAP Hydrates List Pages From Saved Presets
Putting the pieces together, here is the full loop as a user experiences it. It is worth tracing because it shows how a deliberately thin domain produces a noticeably faster workday.
A user opens a list page, say the findings list. The page calls the saved filter endpoint with entity_type=finding, optionally narrowing further by kind. The endpoint returns that user’s finding presets, ordered default-first. The frontend renders them in the saved filters panel and, if one is flagged default, applies it immediately so the list arrives pre-filtered and pre-arranged. The analyst is dropped straight into a working view without a single setup click.
During the session, the analyst can save the current filter and view state as a new preset, rename an existing one, promote one to default, or delete one they no longer use. Each of those is a single endpoint call, scoped to their own user_id, with the sparse-update and owner-enforcement rules keeping the operation safe and minimal. The next time they open the page, their changes are there, and nobody else’s presets were affected.
What makes this work cleanly is that the saved filter domain stays deliberately small. All four of its operations, list, create, update, and delete, are synchronous single-query handlers. There are no background jobs, no events, no async side effects. It is a thin user-preference layer with no dependencies on domain-specific business logic, and it is consumed directly by the frontend to hydrate list-page filter state. That smallness is exactly why it is reliable. It does one thing, owns one table, and never reaches into the entities it serves.
The payoff shows up across the team rather than in any single interaction. The same preset behavior runs on findings, assets, asset groups, projects, scans, companies, reports, dashboards, teams, and tickets. An analyst’s high-and-critical triage view, an asset owner’s exposed-external-assets view, and a manager’s quarterly reporting view all live as personal presets that recall in one action and survive every schema change the underlying lists go through. To see how those standardized views fit alongside the rest of the daily working surface, the configurable dashboards and saved views datasheet walks through the broader pattern.
Frequently Asked Questions
What is the difference between a saved filter and a saved view in PMAP?
Both are saved presets, distinguished by their kind field. A query preset stores filter and search state only, so it controls which rows you see. A view preset stores that same filter state plus presentation state such as column visibility, sort order, and layout, so it controls both which rows you see and how you look at them. The kind is set at creation and is immutable afterward. If you omit or send an invalid kind, PMAP defaults it to view.
Which entity types support saved filters and views?
Ten entity types are supported, validated against a fixed enum: asset, asset_group, finding, project, scan, company, report, dashboard, team, and ticket. Each preset targets exactly one entity type through its entity_type field, and that field is immutable after creation. An invalid entity type is rejected with a 400 Bad Request.
Can I share a saved view with my team?
No. Saved presets are scoped strictly to the owning user through the user_id from the request’s JWT claims. There is no shared or team-level visibility. The list endpoint returns only your own presets, and both update and delete operations enforce ownership with a WHERE id = $1 AND user_id = $2 clause, so you can only modify presets you created. Presets are designed as personal working tools.
How does a default preset work?
Any preset can be flagged is_default = true, which lets the frontend auto-apply it when you open the relevant list page. The list endpoint orders results is_default DESC, updated_at DESC, so defaults appear first and the most recently updated default leads. There is no uniqueness constraint on the default flag, so the database permits multiple defaults for the same user and entity type. When that happens, the default-first ordering still produces a clear answer because the newest default surfaces first.
Will my saved filters break when the filter options on a page change?
No. The criteria and view_state fields are stored as opaque JSON blobs that PMAP saves and returns verbatim without interpreting their internal structure. This decouples the saved filter layer from the schema of every entity it serves, so the underlying list pages can add or change filter dimensions without requiring any change to how presets are stored. An empty criteria value is stored as {} so the payload always remains valid JSON.
Why is there no company_id on saved filters in a multi-tenant platform?
Tenant isolation is achieved implicitly through user_id rather than an explicit company_id column. A user_id is globally unique and bound to a single tenant by the auth layer, so scoping a preset to its owning user automatically scopes it to that user’s tenant. The result is a simpler data model with no separate place a tenant-leak bug could hide, because isolation is inherited from identity.
Can I edit just one attribute of a saved preset without resending the whole thing?
Yes. The update endpoint uses a sparse patch pattern. Only the fields you supply, among name, criteria, view_state, and is_default, are written, and any field you omit keeps its current value. So renaming a preset or promoting it to default touches only that field. As an additional safeguard, an empty criteria value on update is ignored rather than written, so a partial update cannot accidentally wipe a preset’s filter state.