Ten entries and ten thousand are the same screen
A content list stops being useful somewhere around the second page. The library is built for the other end of that — filters you compose rather than pick from a menu, and a view that lives in the URL so you can send it to somebody.
For editors working in a library of thousands, not a blog of twelve
Filters compose, and they reach
A filter is a tree, not a row of dropdowns. Each group carries its own AND/OR toggle and can hold other groups, so "German or French, published, and either missing a cover or missing a summary" is one query rather than three exports and a spreadsheet.
A condition can name a field on a linked entry by its path — author.email, not just author. That is what turns "articles by anyone at this agency" from a two-step hunt into one condition, and the picker groups those fields under the relation they came from so the list stays readable.
Fourteen conditions cover the field types: equals and not-equals, the four comparisons, in and not-in, three text matches, is-null, a range, and a rolling time window. Which ones you are offered depends on the field you picked, because "greater than" on a boolean is a bug waiting to be reported.
The fourteen conditions
| Operator | What it asks |
|---|---|
| eq · ne | Equals, and does not equal. |
| gt · gte · lt · lte | The four comparisons, on numbers, dates and datetimes. |
| in · nin | One of a list, and none of a list. |
| like · ilike | Text match, case-sensitive and case-insensitive. |
| nilike | Text that does not match, case-insensitively. |
| null | Set or unset — the condition that finds the gaps. |
| between | A range on numbers and dates. Written to the API as a pair of comparisons. |
| within_last | A rolling window — “edited in the last 7 days”, measured from when the query runs. |
Rich text is deliberately not filterable or sortable: comparing two documents would compare serialisations of a tree rather than prose. Free-text search still reaches it. within_last is the one condition whose meaning depends on where it is stored — copied into a link it is pinned to a fixed cutoff, so the link keeps showing the same rows, while a saved alarm rule keeps it rolling.
The view is the URL
Search, filter, sort and page all live in the address bar. That has three consequences you feel within a day: the browser Back button does what you expect, a bookmark is a view you never had to name, and sending a colleague a link sends them the screen you are looking at rather than a description of it.
The list is a table you control. Columns can be shown, hidden and reordered, and the collapsed state of a filter is a row of removable chips — "Author · Email contains @lilly ×" — so what is currently narrowing the list is legible without opening anything.
A parameter the server would reject degrades rather than strands you. Ask for page 400 of a 12-page list and you get the last page, not an error screen with nowhere to go.
And when a URL is not enough, save it
A view you return to every morning deserves a name. Saving one stores exactly the slice the URL was carrying — the filter tree, the sort, the page size, the visible columns in their display order, and any parameter a plugin contributed, such as the locale. It reopens on the first page, because a reading position is not part of a slice.
Two things are deliberately left out of what a view stores. Search is not saved — a text match is a one-off question, not a property of the list. Nor is the page number, so a view never opens somewhere you have to scroll back from.
A view is private until you share it. A private one is a personal bookmark over content you can already read, so every role can make one; publishing it to the whole workspace turns it into shared navigation, which is an editorial decision and needs views:share — contributors hold it, viewers do not. You can mark one view your default per list, and that default is yours alone.
What that adds up to
- Bulk actions on the result
- Publish, unpublish or delete everything a filter matched. Publishing a batch re-validates the whole set inside one transaction rather than looping, which is the only thing that closes the gap between "this draft validates" and "publish it".
- The same grammar over the API
- The filter tree the drawer builds is the same JSON your front end sends to
/api/v1. What you can narrow to in the admin, you can fetch from your site. - Scoped to your workspace, always
- Every list query carries the workspace, so there is no view of the library that quietly spans two brands.
- It tells you when it cannot
- If the field definitions have not loaded, the panel says so and disables Apply instead of showing an empty picker over a dead button.
Before a migration
You are moving 4,000 articles to a new template and need the ones that will break: published, in English, with a body under 200 characters or no cover image at all. That is one filter with a nested group, one URL, and a link in the ticket. The person who picks the ticket up sees exactly what you saw.
Questions about working at size
How fast is it on a big table?
List reads are indexed on workspace and status, which is what the common views filter by. That is what keeps a filtered list fast at a hundred thousand records: the conditions people actually use are the ones the indexes cover.
Can I save a filter?
Yes — save the view under a name, and it keeps the filter, sort, page size and columns. Keep it private, or share it with the workspace if you hold views:share. The URL still works for everything you do not want to name.
Does the filter survive switching language?
Yes. Locale is one of the URL parameters, so changing it narrows the same query rather than resetting it.
Can I filter on something inside rich text?
Not with a filter condition. Free-text search reaches the body, because the column is cast to text for the match; structured questions about a document are not something the query builder answers.
Next
Every feature is included, free
The core is MIT licensed and every feature is in it — none of them is paid-only. Community runs free in production; a plan buys room and governance, not a different product.